4、图形引擎入门(上):像素与帧缓冲(Framebuffer)概念、画点、画线、画矩形、颜色模型(RGB565/ARGB8888)
好,咱们正式开始啃图形引擎这块硬骨头。
很多新手一上来就急着学各种炫酷的动画效果,结果连一个像素点都画不明白。我当年也是这样,折腾了三天,屏幕死活不亮,最后发现是帧缓冲地址写错了。嗯,基础不牢,地动山摇。
这一章,咱们就把图形引擎最底层的几个概念彻底搞清楚。说白了,就是搞明白「屏幕上的颜色是怎么来的」以及「怎么让屏幕听你的话」。
4.1 像素:屏幕的最小单元
你看到的任何图像,都是由一个个小方块组成的。这些小方块就是像素(Pixel)。
你可以把屏幕想象成一张巨大的坐标纸。每个格子就是一个像素,每个像素只能显示一种颜色。
- 分辨率:比如 800×480,就是横向有 800 个像素,纵向有 480 个像素。
- 像素密度:同样分辨率下,屏幕尺寸越小,画面越细腻。
在嵌入式 HMI 里,我们通常直接操作像素。你想想看,画一条线,本质上就是把一条直线路径上的所有像素点,都涂上你想要的颜色。
核心认知:图形编程的本质,就是「计算哪些像素需要变色,然后告诉它们变成什么颜色」。
4.2 帧缓冲(Framebuffer):显存的真面目
帧缓冲这个概念,我刚开始学的时候觉得特别玄乎。其实说白了,它就是一块内存区域。
这块内存有多大?它刚好能存下「一帧画面」的所有像素数据。
举个例子:
- 屏幕分辨率:800 × 480
- 颜色格式:RGB565(每个像素占 2 字节)
- 帧缓冲大小 = 800 × 480 × 2 = 768,000 字节 ≈ 750 KB
你的 CPU 或者 GPU 往这块内存里写数据,屏幕的显示控制器就会自动把这块内存里的数据,一行一行地刷新到屏幕上。
我的经验:调试的时候,如果屏幕花屏或者显示错乱,第一件事就是检查帧缓冲的起始地址对不对。我曾经因为地址对齐问题,整整排查了一个下午。
4.3 颜色模型:RGB565 与 ARGB8888
颜色模型决定了「一个像素用多少位来表示」。嵌入式 HMI 里最常用的就两种。
| 颜色模型 | 位数 | 数据格式 | 优点 | 缺点 |
|---|---|---|---|---|
| RGB565 | 16 位 | R:5位 + G:6位 + B:5位 | 省内存,速度快 | 颜色精度稍差 |
| ARGB8888 | 32 位 | A:8位 + R:8位 + G:8位 + B:8位 | 颜色丰富,支持透明 | 内存占用大 |
RGB565:红色用 5 位(0-31),绿色用 6 位(0-63),蓝色用 5 位(0-31)。为什么绿色多一位?因为人眼对绿色最敏感。这是硬件设计上的一个小巧思。
ARGB8888:多了个 Alpha 通道(透明度)。做图标叠加、窗口半透明效果时,必须用它。
避坑指南:我曾经在项目里混用了两种颜色模型,结果画出来的图标颜色完全不对。RGB565 的 0xFFFF 是白色,但 ARGB8888 的 0xFFFFFFFF 才是白色。转换的时候一定要小心。
4.4 画点:一切图形的基础
画点是最基本的操作。你只需要告诉屏幕:在 (x, y) 位置,显示颜色 color。
伪代码实现:
// 假设帧缓冲基地址为 fb_addr,屏幕宽度为 screen_width
// RGB565 格式
void draw_pixel(int x, int y, uint16_t color) {
// 计算像素在帧缓冲中的偏移
uint32_t offset = (y * screen_width + x) * 2;
// 写入颜色数据
*(uint16_t*)(fb_addr + offset) = color;
}
就这么简单。但要注意:
- 一定要做边界检查,防止 x, y 超出屏幕范围。
- 帧缓冲的地址必须是 2 字节对齐的(对于 RGB565)。
4.5 画线:从 Bresenham 算法说起
画线不能简单地用「两点之间取中间点」的思路。因为屏幕上的像素是离散的,你需要找到一条「看起来最直」的像素路径。
Bresenham 算法就是干这个的。它只用整数加减法,没有浮点运算,非常适合嵌入式环境。
核心思想:
- 确定直线的斜率。
- 每次在 x 方向步进 1 个像素。
- 根据误差项决定 y 方向是否步进。
代码示例(简化版):
void draw_line(int x0, int y0, int x1, int y1, uint16_t color) {
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;
while (1) {
draw_pixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
我的习惯:实际项目中,我会把 Bresenham 算法封装成内联函数,减少函数调用开销。画大量线条时,性能差距很明显。
4.6 画矩形:填充与边框
画矩形有两种模式:
- 边框矩形:只画四条边。
- 填充矩形:把整个区域填满颜色。
填充矩形的实现很简单:
void fill_rect(int x, int y, int w, int h, uint16_t color) {
for (int j = y; j < y + h; j++) {
for (int i = x; i < x + w; i++) {
draw_pixel(i, j, color);
}
}
}
但这样效率很低。更好的做法是:
- 计算起始地址。
- 用 memset 或者 DMA 批量填充一整行。
- 循环填充所有行。
性能优化:在 STM32 这类 MCU 上,用 DMA2D 硬件加速填充矩形,速度能快几十倍。后面章节会详细讲。
4.7 知识体系总览
下面这张图,把本章的核心概念串起来了。
4.8 本章小结
这一章的内容,是图形引擎的「地基」。
- 你知道了像素是屏幕的最小单元。
- 你理解了帧缓冲就是一块显存。
- 你学会了 RGB565 和 ARGB8888 的区别。
- 你亲手实现了画点、画线、画矩形的代码。
别小看这些基础操作。后面所有的窗口系统、字体渲染、动画效果,底层都是这些函数在反复调用。把这一章吃透,后面的路会顺很多。