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 算法就是干这个的。它只用整数加减法,没有浮点运算,非常适合嵌入式环境。

核心思想:

  1. 确定直线的斜率。
  2. 每次在 x 方向步进 1 个像素。
  3. 根据误差项决定 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);
        }
    }
}

但这样效率很低。更好的做法是:

  1. 计算起始地址。
  2. 用 memset 或者 DMA 批量填充一整行。
  3. 循环填充所有行。

性能优化:在 STM32 这类 MCU 上,用 DMA2D 硬件加速填充矩形,速度能快几十倍。后面章节会详细讲。

4.7 知识体系总览

下面这张图,把本章的核心概念串起来了。

图形引擎基础概念关系图 帧缓冲 (Framebuffer) 像素 (Pixel) 颜色模型 画点 画线 画矩形 说明: 帧缓冲是内存中的一块区域,存储了每个像素的颜色值。 颜色模型决定了每个像素用多少位来表示。画点/线/矩形都是对帧缓冲的读写操作。

4.8 本章小结

这一章的内容,是图形引擎的「地基」。

  • 你知道了像素是屏幕的最小单元。
  • 你理解了帧缓冲就是一块显存。
  • 你学会了 RGB565 和 ARGB8888 的区别。
  • 你亲手实现了画点、画线、画矩形的代码。

别小看这些基础操作。后面所有的窗口系统、字体渲染、动画效果,底层都是这些函数在反复调用。把这一章吃透,后面的路会顺很多。