绘制第一个三角形:顶点着色器与片段着色器入门
说实话,每次带新人入门 OpenGL ES,我最喜欢的就是这一章。因为从这一章开始,你终于能看到屏幕上出现东西了——不再是枯燥的数学公式,而是一个实实在在的彩色三角形。
我记得自己第一次在 Android 上画出三角形时,盯着屏幕看了好几分钟。虽然只是个简单的三角形,但那种「我能控制 GPU 了」的感觉,真的很奇妙。
1. 着色器到底是什么?
先别急着写代码。我们得搞清楚一件事:着色器(Shader) 到底是什么?
说白了,着色器就是运行在 GPU 上的小程序。CPU 把数据交给 GPU,GPU 用这些小程序来处理数据,最终画出图像。
在 OpenGL ES 中,有两种着色器是必须的:
- 顶点着色器(Vertex Shader):处理每个顶点的位置、颜色等信息
- 片段着色器(Fragment Shader):决定每个像素最终显示什么颜色
我习惯把顶点着色器想象成「画轮廓的人」,片段着色器则是「填色的人」。两者配合,才能画出完整的图形。
核心理解:顶点着色器处理的是「点」,片段着色器处理的是「面」。一个三角形有3个顶点,但可能有成千上万个像素点需要填充颜色。
2. 坐标系统:OpenGL 的世界
你可能会问:为什么我明明传了 (0,0)、(1,0)、(0,1) 三个点,画出来的三角形却不在屏幕左上角?
嗯,这里有个坑。OpenGL 有自己的坐标系统,跟 Android 的屏幕坐标不一样。
OpenGL ES 使用归一化设备坐标(NDC):
- X 轴:-1 到 1(从左到右)
- Y 轴:-1 到 1(从下到上)
- Z 轴:-1 到 1(从近到远)
也就是说,屏幕中心是 (0,0),左上角是 (-1,1),右下角是 (1,-1)。
我曾经有个同事,传了一堆像素坐标进去,结果三角形死活显示不出来。后来发现他忘了做坐标映射——说白了,你得把 Android 的坐标换算成 NDC 坐标。
小技巧:如果你不想手动换算,可以用正交投影矩阵。不过那是后面章节的内容了。现在先硬编码 NDC 坐标,感受一下流程。
3. 编写第一个顶点着色器
顶点着色器用 GLSL(OpenGL Shading Language)编写。这是一种类 C 的语言,但更简洁。
来看一个最简单的顶点着色器:
// 顶点着色器代码
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
解释一下:
attribute:从 CPU 传入 GPU 的顶点属性vec4:四维向量(x, y, z, w)gl_Position:内置变量,表示顶点最终位置
这里有个细节:为什么用 vec4 而不是 vec3?因为 GPU 内部做矩阵运算时,需要齐次坐标。w 通常设为 1.0,表示这是一个点,而不是方向向量。
4. 编写第一个片段着色器
片段着色器更简单。它的任务只有一个:告诉 GPU 每个像素该显示什么颜色。
// 片段着色器代码
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
这段代码的意思是:所有像素都显示红色。
precision mediump float:声明浮点精度。我建议用 mediump,兼顾性能和精度gl_FragColor:内置变量,表示像素颜色vec4(1.0, 0.0, 0.0, 1.0):RGBA,红色通道满值,其他为0,完全不透明
注意:颜色值范围是 0.0 到 1.0,不是 0 到 255。我第一次写的时候习惯性写了 255,结果颜色完全不对。
5. 完整的绘制流程
现在我们把所有东西串起来。在 Android 中绘制一个三角形,需要以下步骤:
- 创建 GLSurfaceView
- 编写并编译着色器
- 创建程序对象,链接着色器
- 准备顶点数据
- 绘制三角形
来看核心代码:
// 顶点数据:三个点,每个点包含 x, y, z
float[] triangleCoords = {
0.0f, 0.5f, 0.0f, // 上顶点
-0.5f, -0.5f, 0.0f, // 左下顶点
0.5f, -0.5f, 0.0f // 右下顶点
};
// 创建缓冲区
ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
FloatBuffer vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(triangleCoords);
vertexBuffer.position(0);
// 编译着色器(省略具体实现)
int vertexShader = loadShader(GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = loadShader(GL_FRAGMENT_SHADER, fragmentShaderCode);
// 创建程序
int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
// 使用程序
GLES20.glUseProgram(program);
// 获取 attribute 位置
int positionHandle = GLES20.glGetAttribLocation(program, "a_Position");
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
这段代码看着长,其实逻辑很清晰:准备数据 → 编译着色器 → 链接程序 → 传入数据 → 绘制。
6. 让三角形变成彩色的
纯红色的三角形太单调了。我们让三个顶点分别显示红、绿、蓝,中间的颜色由 GPU 自动插值。
修改顶点数据,增加颜色属性:
// 顶点坐标
float[] triangleCoords = {
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
// 顶点颜色(RGB)
float[] colorCoords = {
1.0f, 0.0f, 0.0f, // 红色
0.0f, 1.0f, 0.0f, // 绿色
0.0f, 0.0f, 1.0f // 蓝色
};
顶点着色器也要改:
attribute vec4 a_Position;
attribute vec3 a_Color;
varying vec3 v_Color;
void main() {
gl_Position = a_Position;
v_Color = a_Color;
}
片段着色器:
precision mediump float;
varying vec3 v_Color;
void main() {
gl_FragColor = vec4(v_Color, 1.0);
}
这里的关键是 varying 关键字。它表示从顶点着色器传递到片段着色器的变量,GPU 会自动做插值——也就是说,三角形中间每个像素的颜色,都是三个顶点颜色的混合结果。
这就是为什么你能看到渐变效果。 GPU 在光栅化阶段,会自动计算每个像素的颜色值。你不需要手动处理任何插值逻辑。
7. 知识体系总览
下面这张图,是我自己总结的 OpenGL ES 绘制流程。每次带新人我都会画一遍:
8. 避坑指南
我写 OpenGL 这么多年,踩过的坑能写一本书。这里挑几个最常见的:
- 忘记调用 glUseProgram:编译了着色器但没激活,结果屏幕一片黑
- 顶点数据顺序不对:三角形顶点要按逆时针顺序,否则会被剔除
- 忘记设置背景色:默认背景是黑色,三角形也是黑色,你啥也看不见
- 缓冲区没 flip:ByteBuffer 写入数据后要调用 position(0) 重置位置
我曾经花了一整个下午调试一个三角形显示问题。最后发现是着色器代码里少了个分号。GLSL 编译错误不会崩溃,但会静默失败——你看到的只有黑屏。
9. 运行效果
如果你按照上面的代码一步步来,最终会看到一个渐变色的三角形:
- 上顶点:红色
- 左下顶点:绿色
- 右下顶点:蓝色
- 中间区域:三种颜色平滑过渡
这个三角形虽然简单,但它背后涉及了完整的 GPU 渲染管线。你写下的每一行着色器代码,都在直接跟 GPU 对话。
嗯,这就是 OpenGL ES 的魅力所在。