OpenGL基础概念 — 基础篇
一:OpenGL中基础概念
本文聚焦于 OpenGL(Open Graphics Library)基础的渲染概念。了解这些概念后,后续文章将以实际代码演示其操作与用法,帮助你进一步掌握 OpenGL 的使用方式。本文不深入展开 OpenGL 的完整 API 细节,仅围绕 OpenGL 进行渲染所必需的基础概念进行梳理,为后续实战章节理清代码脉络。
1.1 基础前置知识
可以把 OpenGL 想象成一支画笔:它能画出任何东西,但在落笔之前,你必须先明确三个问题——在哪画、如何画、画什么。
在哪画:你需要在"画布"上绘制,但这里的画布不是纸张,而是一个电子屏幕——不同分辨率的屏幕需要不同的系统 API 来对接。这个对接屏幕的抽象层,专业术语叫窗口(Window),不同操作系统提供不同的窗口 API。在 Android 平台上,这个窗口对接工作由EGL(Embedded-System Graphics Library)完成。EGL 负责配置系统的渲染表面,配置完成后你就拥有了一块可绘制的画布。
这块画布可以是可见的(最终显示到屏幕上),也可以是不可见的。不可见的画布对应渲染管线中的帧缓冲对象(FBO,Framebuffer Object)。只要你的绘制结果不需要立刻上屏,就可以一直在 FBO 上操作,直到最后一步才输出到屏幕。
如何画:这对应你编写的 OpenGL 程序。程序编译通过后,相当于你手握一支能按预期工作的画笔。VAO(Vertex Array Object,顶点数组对象)、VBO(Vertex Buffer Object,顶点缓冲对象)等概念,就是你在程序中需要操控的核心组件。
画什么:你可以画一张图片、一个 3D 模型、或一段线段。无论画什么,最终呈现在屏幕上都要经过图形渲染管线:顶点数据 → 图形装配(Primitive Assembly) → 光栅化(Rasterization) → 屏幕输出。这个管线中有些阶段你可以编程控制,有些阶段是 GPU 硬件固定实现的——你只需要关注你能控制的部分即可。
你画的内容在绘制之前就已经存在了,不会凭空产生。例如:
- 画一张图片(JPEG、Bitmap 等格式)→ 借助**纹理(Texture)**承载图像数据
- 画一段视频帧→ 解码后的帧数据通过纹理刷新到 GPU
- 画一条线段→ 通过顶点数据定义线段的端点,控制坐标系将它们连接起来
- 画一个3D 模型(如 .obj 文件) → 加载顶点数据,通过 MVP 矩阵控制展示为 3D 或 2D 效果(即使以 2D 形式展示,物体本身仍是 3D 数据)
很多初学 OpenGL 的人之所以感到困惑,正是因为图形绘制知识体系与传统的 UI 开发模式有显著差异,导致不清楚这三点。因此在开始学习 OpenGL 时,请始终牢记:在哪画、如何画、画什么。
举个例子:使用 OpenGL 绘制一张图片。
传统 Android 方式:创建一个
ImageView,设置图片资源,控件自动完成渲染。OpenGL 方式:
- 在哪画:Android 提供了
GLSurfaceView等控件,封装了 EGL 环境与窗口系统的对接。 - 如何画:你需要实现自己的 OpenGL 渲染程序(Shader、顶点数据等)。
- 画什么:将图片加载为纹理(Texture)并绑定到 OpenGL,由 Shader 采样输出到屏幕。
- 在哪画:Android 提供了
注:OpenGL 是跨平台的通用图形渲染标准。OpenGL ES 是面向嵌入式设备(手机、平板等)的精简子集,WebGL 是面向浏览器的 Web 版本,三者的核心渲染机制相同,API 和 Shader 语法有所差异。
1.2 顶点坐标和纹理坐标
顶点坐标(Vertex Coordinates)和纹理坐标(Texture Coordinates)是 OpenGL 绘制中必须指定的两组坐标系。
顶点坐标:定义图形每个顶点在画布上的位置,在上文的代码中直接使用了 NDC(Normalized Device Coordinates,归一化设备坐标)范围[-1, 1]。纹理坐标:定义要采样纹理的哪一部分,范围通常是归一化的[0, 1]。
/** * 方式一:顶点坐标和纹理坐标合并在一个数组中 * 顶点坐标(x, y),范围[-1, 1];纹理坐标(u, v),范围[0, 1] */protectedfloat[]mVertexAndTex={-1f,-1f,0f,1f,// 左下:顶点(-1,-1), 纹理(0,1)1f,-1f,1f,1f,// 右下:顶点(1,-1), 纹理(1,1)-1f,1f,0f,0f,// 左上:顶点(-1,1), 纹理(0,0)1f,1f,1f,0f,// 右上:顶点(1,1), 纹理(1,0)};/** * 方式二:顶点坐标和纹理坐标分开存储 */protectedfloat[]mVertex={-1f,-1f,// 左下1f,-1f,// 右下-1f,1f,// 左上1f,1f,// 右上};protectedfloat[]mTex={0f,1f,// 左下1f,1f,// 右下0f,0f,// 左上1f,0f,// 右上};注:代码示例为 Java,C++ 中定义方式类似。
如果顶点坐标不在[-1, 1]的 NDC 范围内——比如模型本身有自己的局部坐标系(Local Space)——就需要通过 MVP 矩阵(Model-View-Projection,模型-观察-投影矩阵)将顶点从局部坐标系依次变换到世界坐标系、观察坐标系、裁剪坐标系,最终映射到 NDC 屏幕空间。这一变换链路是 3D 渲染的基础,在纯 2D 渲染场景中大部分情况下可以省略。
1.3 顶点着色器和片段着色器
顶点着色器(Vertex Shader)— OpenGL 2.0 / GLSL 1.10 语法:
attribute vec4 aPosition; // 顶点位置(从CPU传入) attribute vec2 aTexCoord; // 纹理坐标(从CPU传入) varying vec2 vTexCoord; // 传递给片段着色器的纹理坐标 void main() { vTexCoord = aTexCoord; gl_Position = aPosition; // 直接使用NDC坐标,不做变换 }片段着色器(Fragment Shader)— OpenGL 2.0 / GLSL 1.10 语法:
#extension GL_OES_EGL_image_external : require // 启用OES外部纹理扩展 precision mediump float; // 指定float精度 varying vec2 vTexCoord; // 从顶点着色器接收的纹理坐标 uniform samplerExternalOES uTexture; // OES外部纹理采样器 void main() { gl_FragColor = texture2D(uTexture, vTexCoord); }顶点着色器的主要作用是对输入的顶点数据进行处理。原始顶点数据存储在模型文件或内存缓冲区中,渲染时需要将其传递到 GPU。数据到达顶点着色器时,说明已经在 GPU 端了——你需要在此阶段编写程序对每个顶点进行变换计算(如 MVP 矩阵变换),并将结果输出给渲染管线的后续阶段。
片段着色器可以理解为:为每一个待输出的像素(更准确地说,是光栅化后生成的每一个片元 Fragment)执行一次计算,决定该像素最终的颜色值。例如你看到一个纯红色的正方形,片段着色器会将覆盖范围内的每个片元都设置为红色——在 GLSL 中,红色表示为(1.0, 0.0, 0.0)(归一化浮点数,而非 0-255 的整数值),而正方形的显示范围则由之前的顶点坐标决定。
1.4 VAO 和 VBO
- VAO(Vertex Array Object,顶点数组对象):存储顶点属性配置状态——包括哪些属性启用、各自的格式、以及绑定了哪个 VBO。注意:VAO 是 OpenGL 3.0+ 才支持的特性,OpenGL 2.0 中没有 VAO。
- VBO(Vertex Buffer Object,顶点缓冲对象):GPU 端的原始数据缓冲区,用于存储顶点坐标、纹理坐标、法线等数据。
- EBO(Element Buffer Object,元素缓冲对象)/ IBO(Index Buffer Object,索引缓冲对象):存储顶点索引,用于复用顶点数据,减少重复传输。
VAO 和 VBO 各有分工、互相配合:VBO 负责"装数据",VAO 负责"描述数据格式"。你在 1.2 节看到的顶点坐标数组,需要通过 VBO 上传到 GPU,再由 VAO(或直接通过glVertexAttribPointer)告诉 GPU 如何解析这些数据。
顶点数据 → [VBO存储] → [VAO描述格式] → GPU渲染管线1.5 FBO(帧缓冲对象)
FBO(Framebuffer Object,帧缓冲对象)是 OpenGL 提供的离屏渲染(Off-screen Rendering)机制。
正常情况下,渲染结果会直接输出到屏幕。但有时我们需要做多步后处理,中间步骤不需要被用户看到——这就是 FBO 的用武之地。你可以将 FBO 理解为一块不上屏的画布:每当执行一次 Draw Call,画面就渲染到 FBO 绑定的纹理上,而非屏幕。
典型场景:给视频画面依次添加水印、饱和度调节和签名。这三步中只有最后一步需要上屏:
- 渲染原始画面 → FBO-A(绑定纹理 A)
- 对纹理 A 做饱和度调节 → FBO-B(绑定纹理 B)
- 对纹理 B 叠加签名 → 直接输出到屏幕
每一步的 FBO 实际上就是绑定了一张纹理,处理结果就写在这张纹理里,供下一步使用。
此外,FBO 也是调试利器:你可以将中间渲染结果输出到 FBO 绑定的纹理,然后读取到 CPU 端——导出为 JPEG 文件、显示到 Bitmap 上,或在 Debug 模式下逐帧检查。这在排查渲染问题时非常有用。
1.6 MVP 矩阵
MVP 矩阵由三个矩阵组成:Model(模型矩阵)、View(观察矩阵)、Projection(投影矩阵)。它们的核心作用是将 3D 物体的顶点从局部坐标系变换到屏幕坐标系。
但在纯 2D 渲染场景(如渲染视频帧或单张图片)中,顶点坐标通常直接用 NDC 范围[-1, 1],MVP 矩阵并非必需。
注:以下代码采用 C++ 的 GLM(OpenGL Mathematics)数学库作为示例。
M 矩阵(Model Matrix,模型矩阵)— 处理物体的平移、旋转、缩放:
// 沿 Z 轴负方向平移 3 个单位glm::mat4 model=glm::mat4(1.0f);model=glm::translate(model,glm::vec3(0.0f,0.0f,-3.0f));// 绕 X 轴逆时针旋转 55 度model=glm::rotate(model,glm::radians(-55.0f),glm::vec3(1.0f,0.0f,0.0f));// 沿 X 轴缩放 2 倍model=glm::scale(model,glm::vec3(2.0f,1.0f,1.0f));V 矩阵(View Matrix,视图矩阵)— 将顶点从世界坐标系变换到观察坐标系:
glm::mat4 view=glm::mat4(1.0f);view=glm::translate(view,glm::vec3(0.0f,0.0f,-3.0f));P 矩阵(Projection Matrix,投影矩阵)— 有两种:正交投影和透视投影。正交投影建立一个长方体区域,落在内部的顶点才会被绘制,外部的被裁切。透视投影以"近大远小"的视椎体为裁剪区域。
// 正交投影矩阵 — 适合 2D 渲染或 UIglm::mat4 proj=glm::ortho(0.0f,// left1920.0f,// right0.0f,// bottom1080.0f,// top-1.0f,// near1.0f// far);// 透视投影矩阵 — 适合 3D 场景glm::mat4 proj=glm::perspective(glm::radians(45.0f),// FOV:视野角度16.0f/9.0f,// 宽高比0.1f,// 近裁剪面距离100.0f// 远裁剪面距离);1.7 纹理(Texture)
纹理(Texture)可以简单理解为 GPU 端存储的一张图像。平时看到的 JPEG、PNG 等格式的图片,在 OpenGL 中展示时,本质上是将图像数据上传为纹理,再通过 Shader 采样输出到屏幕。
unsignedinttexture;// 纹理句柄(ID)glGenTextures(1,&texture);// 生成一个纹理对象glBindTexture(GL_TEXTURE_2D,texture);// 绑定为2D纹理// 设置纹理参数glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);// S轴(水平)环绕模式:重复glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);// T轴(垂直)环绕模式:重复glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);// 缩小时的过滤方式:线性插值glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);// 放大时的过滤方式:线性插值// 上传图像数据到GPUglTexImage2D(GL_TEXTURE_2D,0,GL_RGB,width,height,0,GL_RGB,GL_UNSIGNED_BYTE,data);二:OpenGL中的Shader编程概念
2.1 渲染管线流程
在深入 Shader 编程之前,先了解**图形渲染管线(Graphics Pipeline)**的整体流程。渲染管线是将 3D/2D 数据最终转换为屏幕像素的一系列处理阶段:
OpenGL 渲染管线 ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ 顶点数据 │ │ (VBO/VAO) │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 顶点着色器 │ ◄── 可编程 ★ │ │ │ Vertex Shader│ 对每个顶点执行变换(MVP、坐标转换) │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 几何着色器 │ ◄── 可编程(可选) │ │ │Geometry Shader│ 对图元级别进行修改/生成 │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 图形装配 │ ◄── GPU 硬件固定 │ │ │Primitive Assy │ 将顶点组装为图元(点、线、三角形) │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 光栅化 │ ◄── GPU 硬件固定 │ │ │Rasterization │ 将图元转换为片元(Fragment) │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 片段着色器 │ ◄── 可编程 ★ │ │ │Fragment Shader│ 为每个片元计算最终颜色值 │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 测试与混合 │ ◄── GPU 硬件(可配置) │ │ │Test & Blend │ 深度测试、模板测试、Alpha混合 │ │ └──────┬───────┘ │ │ │ │ │ ▼ │ │ 屏幕输出 │ │ │ └─────────────────────────────────────────────────────────────────────┘ ★ = 开发者可编程控制的核心阶段关键要点:
- 顶点着色器和片段着色器是渲染管线的两个必经可编程阶段,也是开发者最常编写代码的地方。
- 几何着色器是可选的,位于顶点着色器之后、光栅化之前,初学者可暂时跳过。
- 图形装配和光栅化由 GPU 硬件自动完成,开发者无法修改其内部逻辑,但可以通过 API 配置其行为(如指定图元类型为
GL_TRIANGLES还是GL_LINES)。 - 测试与混合阶段可配置(如开启/关闭深度测试、设置 Alpha 混合模式),但不属于 Shader 编程范畴。
理解渲染管线是学习 Shader 的前提——你需要知道数据从哪来、经过哪些处理、最终去了哪里,才能写出正确的 Shader 代码。
2.2 概念介绍
在 OpenGL 渲染管线中,共有三种可编程的着色器(Shader):
- 顶点着色器(Vertex Shader)— 处理顶点数据,位于几何阶段
- 几何着色器(Geometry Shader)— 处理图元级别的生成与修改(可选阶段,OpenGL 3.2+ 才支持)
- 片段着色器(Fragment Shader,也称像素着色器 Pixel Shader)— 处理每个片元的颜色输出,位于光栅化阶段
其中顶点着色器和片段着色器是必选的,几何着色器是可选的。
顶点着色器处理每个顶点的变换计算,典型的应用场景是 MVP 矩阵变换(旋转、平移、缩放、投影)。它的输入是顶点坐标、纹理坐标等原始属性,输出是变换后的顶点坐标(gl_Position)以及传递给片段着色器的插值数据(如纹理坐标)。
几何着色器在顶点着色器之后、光栅化之前执行,可对图元(点、线、三角形)进行修改或生成新的图元。它在渲染管线中是可选的。初学者如果没有特定需求可以暂时跳过。需要说明的是,几何着色器并未被"取代"——桌面端 OpenGL 3.2+ 仍支持它;只是在 Vulkan、Metal 等新一代 API 中,Mesh Shader 提供了更灵活的替代方案。
在复杂场景的 OpenGL 渲染中,顶点着色器和片段着色器是开发者主要编程的两个阶段。3D 场景的 Shader 通常较为复杂,包含光照计算、阴影映射等;而 2D 场景的 Shader 则相对简单,以纹理采样和色彩处理为主。
Shader 语法在不同版本间有差异(如 OpenGL 2.0 / GLSL 1.10 使用attribute/varying,OpenGL 3.3+ / GLSL 330+ 使用in/out),但核心机制不变:顶点着色器负责输出变换后的顶点位置及插值数据(纹理坐标、颜色、法线等),片段着色器负责为每个片元计算最终的 RGBA 颜色值。
2.3 关键参数介绍
顶点着色器的关键参数说明:
// === OpenGL 3.3 / GLSL 330 版本 === #version 330 core layout(location = 0) in vec3 aPosition; // 顶点位置(location=0) layout(location = 1) in vec2 aTexCoord; // 纹理坐标(location=1) layout(location = 2) in vec3 aColor; // 顶点颜色(location=2) uniform mat4 uModel; // Model 矩阵——CPU通过uniform传入 uniform mat4 uView; // View 矩阵 uniform mat4 uProjection; // Projection 矩阵 out vec3 vColor; // 传递给片段着色器的颜色(插值后) out vec2 vTexCoord; // 传递给片段着色器的纹理坐标 void main() { // MVP 变换:局部坐标 → 裁剪坐标 gl_Position = uProjection * uView * uModel * vec4(aPosition, 1.0); vTexCoord = aTexCoord; vColor = aColor; }三个关键修饰符:
in(OpenGL 3.3+)/attribute(OpenGL 2.0):从 CPU 端传入顶点着色器的属性数据uniform:CPU 端传入的全局变量,一次 Draw Call 内所有顶点共享(如 MVP 矩阵)out(OpenGL 3.3+)/varying(OpenGL 2.0):从顶点着色器输出,传递给片段着色器(值会被插值)
片段着色器的关键参数说明:
// === OpenGL 3.3 / GLSL 330 版本 === #version 330 core in vec3 vNormal; // 从顶点着色器接收的法线(已插值) in vec2 vTexCoord; // 从顶点着色器接收的纹理坐标(已插值) in vec3 vColor; // 从顶点着色器接收的颜色(已插值) uniform sampler2D uTexture; // 2D纹理采样器 out vec4 fragColor; // 最终输出的片元颜色(RGBA) void main() { vec3 normal = normalize(vNormal); // 法线归一化 vec4 texColor = texture(uTexture, vTexCoord); // 从纹理采样 // 简单漫反射光照:法线Z分量越大,越朝向相机,越亮 float diffuse = max(dot(normal, vec3(0.0, 0.0, 1.0)), 0.0); fragColor = texColor * diffuse; // 纹理色 × 光照强度 }三:OpenGL的相关坐标系概念
3.1 坐标系的种类
- 局部坐标系(Local Space):建模软件中物体自身的坐标系,每个模型以各自的参考点为原点,互不关联。
- 世界坐标系(World Space):将多个模型放置到同一个 3D 场景中时使用的全局坐标系,各物体通过 Model 矩阵确定在世界中的位置、旋转和缩放。
- 观察坐标系(View Space):以摄像机(Camera)为原点,摄像机的视线方向为 -Z 轴、上方为 +Y 轴、右侧为 +X 轴的坐标系,由 View 矩阵完成变换。
- 裁剪坐标系(Clip Space):一个 4D 齐次坐标空间
(x, y, z, w),由 Projection 矩阵变换得到,超出[-w, w]范围的顶点将被裁剪(不渲染)。 - NDC(Normalized Device Coordinates,归一化设备坐标):裁剪坐标经过透视除法(除以 w 分量)后得到的坐标空间,范围为
[-1, 1]。NDC 与屏幕分辨率无关。 - 屏幕坐标系(Screen Space):最终映射到物理屏幕像素的坐标系,由
glViewport函数决定 NDC 到实际像素的映射。例如glViewport(0, 0, 1080, 1920)将 NDC 映射到 1080×1920 像素区域。
3.2 坐标系的关系
局部坐标系中的数据就是建模时的原始顶点数据。以backpack.obj文件为例,用文本编辑器打开可以看到一系列顶点坐标——这些坐标都是相对于建模软件中的坐标原点定义的,这就是局部坐标系。不同的人在不同建模软件中创建的模型,各自拥有独立的坐标原点,互不关联,因此称为"局部"坐标系。
世界坐标系是你定义的 3D 场景的全局坐标空间。在编写 OpenGL 程序之前,你需要决定每个物体在 3D 世界中的摆放位置。例如,你加载了一个桌子模型,希望它出现在场景的左下方——通过 Model 矩阵设置平移分量,将桌子的局部坐标变换到世界坐标的目标位置。此时,世界坐标原点是你在场景中任意选定的一点。
在世界坐标系中摆放好物体后,需要将整个场景变换到观察坐标系(也称相机坐标系),以简化后续的投影计算。观察坐标系以摄像机的位置为原点,视线方向(看向屏幕内部)为 -Z 轴,摄像机的上方为 +Y 轴,右侧为 +X 轴。这一变换由 View 矩阵完成。
裁剪坐标系是一个关键的中间阶段。经过 Projection 矩阵变换后,顶点进入裁剪空间。在这个空间中,OpenGL 可以判断哪些顶点在视野范围内(需要渲染)以及哪些在视野范围外(被裁剪掉,不渲染)。判断依据是:顶点的 x、y、z 分量是否在[-w, w]范围内。之后经过透视除法(除以 w),裁剪坐标转化为 NDC。
NDC 坐标(范围[-1, 1],原点在中心)经过glViewport映射后,最终转换为屏幕像素坐标。glViewport(0, 0, 1080, 1920)表示将 NDC 映射到一个 1080×1920 像素的区域;glViewport(0, 0, 540, 960)则映射到 540×960 像素区域。这意味着同样的 NDC 内容可以绘制到不同大小的屏幕上,由glViewport决定最终输出的像素范围。
各个坐标系之间的转换关系:
局部空间 ──×Model──→ 世界空间 ──×View──→ 观察空间 ──×Projection──→ 裁剪空间 ──÷w(透视除法)──→ NDC ──glViewport──→ 屏幕空间总结各坐标系的维度:
- 局部 / 世界 / 观察坐标系— 3D 笛卡尔坐标系(x, y, z)
- 裁剪坐标系— 4D 齐次坐标系(x, y, z, w)
- NDC— 3D 坐标(x, y, z),z 保留用于深度测试
- 屏幕坐标系— 2D 像素坐标系(通过
glViewport映射后)
3.3 坐标系变换图解
为了让读者更直观地理解各个坐标系之间的变换过程,下面用一个生活化的场景来类比:
场景:你在客厅里拍照
坐标系变换全景图 ┌──────────────────────────────────────────────────────────────────────────┐ │ │ │ ① 局部坐标系 ② 世界坐标系 ③ 观察坐标系 │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ 沙发模型 │ Model矩阵 │ 客厅中的沙发 │ View矩阵 │ 相机看到的 │ │ │ │ 出厂坐标 │ ────────→ │ 摆放好位置 │ ────────→ │ 场景(视锥) │ │ │ │ (厂内原点) │ │ (客厅原点) │ │ (相机原点) │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ │ │ │ │ ▼ Projection矩阵 │ │ ┌────────────┐ │ │ │ ④ 裁剪坐标系 │ │ │ │ 决定哪些可见 │ │ │ │ (视锥体裁剪) │ │ │ └────────────┘ │ │ │ │ │ ▼ 透视除法(÷w) │ │ ┌────────────┐ │ │ │ ⑤ NDC │ │ │ │ 归一化坐标 │ │ │ │ [-1, 1]范围 │ │ │ └────────────┘ │ │ │ │ │ ▼ glViewport │ │ ┌────────────┐ │ │ │ ⑥ 屏幕坐标系 │ │ │ │ 实际像素输出 │ │ │ │ (如 1080p) │ │ │ └────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────────┘场景类比解析:
你现在要拍一张客厅的照片,照片内容就是 OpenGL 最终渲染到屏幕上的画面。整个过程可以拆解为以下步骤:
| 步骤 | 坐标系 | 类比动作 | 对应矩阵 |
|---|---|---|---|
| 1 | 局部坐标系 | 沙发在出厂时,厂家记录了它在工厂中的位置——这个位置只在工厂内部有意义 | — |
| 2 | 世界坐标系 | 你把沙发搬回家,摆到客厅的东北角——沙发现在在你的"世界"里有了确定的位置 | × Model |
| 3 | 观察坐标系 | 你站在客厅门口,举起相机——相机的位置和朝向决定了能看到什么 | × View |
| 4 | 裁剪坐标系 | 取景框范围内的景物会被拍下来,范围外的(比如背后的墙)不会出现在照片中 | × Projection |
| 5 | NDC | 照片的构图比例被确定下来,与你使用什么尺寸的相纸无关 | ÷ w(透视除法) |
| 6 | 屏幕坐标系 | 你把照片冲印到 6 寸相纸或 10 寸相纸上——同样的构图,不同的物理尺寸 | glViewport |
关键记忆点:变换链路为Local → World → View → Clip → NDC → Screen,其中前三步由矩阵乘法完成(× Model → × View → × Projection),后两步由 GPU 硬件自动处理(÷ w → glViewport 映射)。
四:关键术语速查表
| 术语 | 英文全称 | 简要解释 | 章节 |
|---|---|---|---|
| OpenGL | Open Graphics Library | 跨平台的 2D/3D 图形渲染 API 标准 | 1.1 |
| GLSL | OpenGL Shading Language | OpenGL 着色器编程语言,语法类似 C | 1.3 |
| EGL | Embedded-System Graphics Library | 连接 OpenGL 与操作系统窗口系统的中间层 | 1.1 |
| NDC | Normalized Device Coordinates | 归一化设备坐标,范围 [-1, 1],与屏幕分辨率无关 | 1.2 |
| VBO | Vertex Buffer Object | GPU 端的顶点数据缓冲区 | 1.4 |
| VAO | Vertex Array Object | 存储顶点属性配置状态(格式、绑定关系) | 1.4 |
| EBO / IBO | Element/Index Buffer Object | 存储顶点索引,用于复用顶点减少数据量 | 1.4 |
| FBO | Framebuffer Object | 帧缓冲对象,支持离屏渲染(不直接输出到屏幕) | 1.5 |
| MVP | Model-View-Projection | 模型-观察-投影矩阵,将顶点从局部坐标变换到裁剪坐标 | 1.6 |
| Vertex Shader | — | 顶点着色器,处理每个顶点的变换计算 | 1.3 / 2.2 |
| Fragment Shader | — | 片段着色器(像素着色器),计算每个片元的颜色值 | 1.3 / 2.2 |
| Geometry Shader | — | 几何着色器,可选阶段,对图元进行修改或生成 | 2.2 |
| Texture | — | 纹理,GPU 端存储的图像数据 | 1.7 |
| Fragment | — | 片元,光栅化后生成的候选像素,经过测试后才成为最终像素 | 1.3 |
| Rasterization | — | 光栅化,将图元(三角形/线/点)转换为片元的过程 | 2.1 |
| Primitive | — | 图元,OpenGL 的基本绘制单位:点、线、三角形 | 2.1 |
| Draw Call | — | 一次绘制指令,将一组顶点数据提交给 GPU 渲染 | 1.5 |
| Uniform | — | GLSL 修饰符,表示一次 Draw Call 内所有顶点共享的全局变量 | 2.3 |
| Attribute / in | — | GLSL 修饰符,表示从 CPU 端传入每个顶点的属性数据 | 2.3 |
| Varying / out | — | GLSL 修饰符,表示从顶点着色器输出并传递给片段着色器的插值数据 | 2.3 |
| Viewport | — | 视口,由glViewport定义,决定 NDC 到屏幕像素的映射区域 | 3.1 |
| GLM | OpenGL Mathematics | C++ 数学库,提供向量和矩阵运算(仅头文件) | 1.6 |
| Orthographic Projection | — | 正交投影,无近大远小效果,适合 2D 渲染 | 1.6 |
| Perspective Projection | — | 透视投影,有近大远小效果,适合 3D 渲染 | 1.6 |
五:参考资料
- 3D 模型示例:backpack.obj