GFX 深入:第一部分 - 绘图基础





5.00/5 (7投票s)
探索使用 GFX 进行绘图的技术。
此代码未积极维护。维护整个系列的工作量太大。这意味着如果您想获取最新的 GFX,请参阅以下文章
引言
本系列的下一篇文章在此。
GFX 是一项庞大的工作,通过一个有时表面上简单但非常深入的 API 暴露了许多功能。然而,为了在提供合理性能的同时促进其众多功能,GFX 在设计时采用了与大多数图形库不同的编程范式。
大多数 C++ 图形库都是面向对象的。GFX 有对象,但它们不是核心。GFX 通过基于**泛型编程**的 API 暴露其功能。幸运的是,您无需成为专家即可使用 GFX,但它确实有一些前期学习曲线。本文是系列文章的第一部分,旨在帮助您掌握 GFX,从基础到高级。
构建这个大杂烩
您需要安装了 Platform IO 扩展的 Visual Studio Code。您需要一个连接了 ILI9341 LCD 显示屏的 ESP32。如果您真的愿意,可以修改代码以使用不同的驱动程序。
我推荐 乐鑫 ESP-WROVER-KIT 开发板,它集成了 ILI9341 显示屏和几个其他预接外设,以及一个集成调试器和更快的 USB 转串口桥接器,上传速度更快。它们可能比标准的 ESP32 开发板更难找到,但我在 JAMECO 和 Mouser 上找到它们,价格约为 40 美元。如果您进行 ESP32 开发,它们非常值得投资。集成调试器虽然比 PC 慢得多,但比您使用连接到标准 WROVER 开发板的外部 JTAG 探针所能获得的要快。
然而,你们大多数人将使用通用 esp32 配置。在 VS Code 蓝色条的屏幕底部,有一个配置切换器。它应该默认为“Default”开始,但您可以通过单击“Default”来更改它。屏幕顶部会弹出一个包含两个配置的列表。从那里,您可以选择您的设置。
为了连接所有这些,请参阅 *wiring_guide.txt*,其中包含 SPI 显示器的接线说明。请记住,有些显示器供应商使用非标准名称命名其引脚。例如,在某些显示器上,`MOSI` 可能被标记为 `DIN` 或 `A0`。您可能需要通过谷歌搜索来查找您设备的详细信息。
注意:Platform IO IDE 有时有点难搞。第一次打开项目时,您可能需要转到左侧的 Platform IO 图标 - 它看起来像一个外星人。单击它打开侧边栏,然后在 *快速访问|其他* 下查找 *Platform IO Core CLI*。单击它,然后在出现提示时,键入 `pio run` 以强制它下载必要的组件并构建。您不应该再需要这样做,除非您在尝试构建时再次开始收到错误。另外,出于某种原因,每当您切换配置时,您都必须去刷新(“项目任务”旁边的小圆圈箭头)它才会生效。
概念化这个混乱的局面
有关 GFX 从高级到代码的完整处理,请参阅此链接文章,它作为其主要文档。在这里,我们将深入探讨并专注于最基本的绘图原语,但会添加 Alpha 混合和离屏帧缓冲以使其更具趣味性。
像素大显身手!
GFX 具有一种新颖的方式来表示像素和颜色。它们具有任意定义的颜色模型(RGB、YUV、灰度等)、任意定义的位深/分辨率(1 位、16 位、24 位等)以及任意定义的与颜色模型相关并定义颜色模型的命名通道数量。
本质上,不同的媒体有不同的格式。JPEG 以 24 位 Y'CbCr BT.601 格式表示其像素,而典型的彩色物联网显示设备是 16 位 RGB(甚至是 18 位 RGB 以 24 位带填充表示,有时甚至是带调色板/CLUT 的索引颜色)。此外,某些媒体支持具有表示半透明颜色的能力的 Alpha 通道。
所有这些至少令人眼花缭乱。您如何开始管理它?
首先,GFX 在必要时无缝地在像素格式之间进行转换,同时进行 Alpha 混合,因此您通常不必担心显式地在不同格式之间进行转换。
其次,`pixel<>` 模板提供了一个丰富的 API,但只有少数几个核心成员需要您担心。尽管如此,像素提供了一个丰富的模板接口,允许您指定像素每个通道的细节,从中它会在编译时计算所有其余信息。您通常不必以这种方式显式定义像素,因为它们要么通过某个绘制目标的 `pixel_type` 成员预声明给您,要么当您无法这样做时,有包装器可以非常简单地声明常见的像素格式,如下所示。
考虑这些例子:要声明一个像 .NET 中使用的 32 位 RGBA 像素,您只需使用 `rgba_pixel<32>`。要声明一个像许多物联网显示器中使用的 16 位 RGB 像素,请使用 `rgb_pixel<16>`。要声明一个 8 位灰度像素,请使用 `gsc_pixel<8>`。要声明一个单色像素,您可以使用 `gsc_pixel<1>`。要声明一个 JPEG 的 Y'CbCr 格式的像素,您可以使用 `ycbcr_pixel<24>`。
接下来,像素总是通过通道的概念来表示。通道是命名和索引的值,对应于像素的颜色和显示信息,以及其二进制布局。例如,用 `rgb_pixel<16>` 声明的 16 位像素将有三个颜色通道——`R`、`G` 和 `B`。`R` 是 5 位,`G` 是 6 位,`B` 是 5 位。绿色占用剩余的位,因为大多数像素格式将任何额外的位分配给绿色,因为我们的眼睛比其他颜色更能辨别绿色。这些通道上不同的位深度会产生不同的通道值范围。例如,`R` 和 `B` 各有 5 位,有效范围是 0-31,而 `G` 有 6 位,有效范围是 0-63。不过,您不必担心这些细节,因为您总是可以将其值作为介于 0 和 1 之间的浮点数获取。您可以通过名称或索引访问通道。获取通道基本上是 `the_pixel.channel<{名称或索引}>()` 用于整数,或者 `the_pixel.channelr<{名称或索引}>()` 用于缩放的浮点数(实数)值。设置它们也类似:`the_pixel.channel<{名称或索引}>({新值})` 用于整数,或者 `the_pixel.channelr<{名称或索引}>({新值})` 用于缩放的浮点数(实数)值。您还可以使用 `the_pixel.value()` 获取像素的整个值作为包含所有通道数据的字,并使用 `the_pixel.value({新值})` 设置它。可以使用 `the_pixel.native_value` 访问机器顺序字。别担心,我们稍后会看到它的实际应用。
每当像素具有*Alpha 通道*(`channel_name::A`)时,GFX 将尝试根据 Alpha 通道的值将其颜色与它下面的任何颜色进行混合。例如,如果 Alpha 通道为 `0.75` 且绘制颜色为 `red`,则红色将与它下面的任何颜色以 3/4 偏向红色,1/4 偏向下面的颜色混合。
每次需要白色时都手动声明所有颜色,通过设置像素的每个通道,那将是一种遗憾。幸运的是,GFX 提供了所有标准 X11 命名颜色,作为您所需的任何像素格式的预定义颜色。暂时不要问这种魔法是如何工作的。它很酷,但也是我们现在不需要的旁枝末节。底线是,您可以使用 `my_colors = color
还有一个丰富的 API 用于确定像素有哪些通道、有多少个通道、它们的顺序是什么,甚至比较两种像素类型以查看它们是否共享一个颜色模型。这些大部分您永远都不需要,所以我们在这里不会介绍。如果您想扩展 GFX 以便能够在其他颜色模型和 RGB 之间进行转换,您才会使用它。这个兔子洞确实很深。
大多数绘图方法都接受一个像素,该像素指示绘图操作要使用的颜色。例如,`draw::line<>()` 接受一个像素作为绘制线条的颜色。您想要向 `draw::` 方法提供什么类型的像素都没有关系。它们会接受任何东西并进行必要的魔法使其工作。例如,如果您将一个 32 位 RGBA 像素传递给一个 16 位 RGB 位图,像素将自动降采样到 16 位,并与位图中的底层像素进行 Alpha 混合。它甚至会在某些常见的颜色模型(如 Y'UV、灰度和 RGB)之间进行转换。这通常是您实现格式转换和 Alpha 混合的方式。
像素是一个看似简单却功能强大的小工具。上面的内容可能会让它们看起来很复杂。事实是,它们确实很复杂。然而,同样,在您需要它之前,您无需为大部分复杂性而烦恼,这对于 GFX 的日常使用来说是不太可能的。当我们看到代码时,您会发现使用像素非常简单。
就位
位置在房地产中很重要,在屏幕空间中也是如此。我们需要方法来指定绘图操作的位置,通常还有尺寸。定位 API 提供了您为此所需的所有工具。
Points
一个点仅仅是一个二维坐标。它由 `x` 和 `y` 值组成,根据点的类型,这些值可以是带符号或无符号的。有一些成员用于偏移点,以及查看一个点是否与另一个点相交。也像大多数位置对象一样,点可以在带符号和无符号版本之间进行转换。您通常会使用 `point16` 用于无符号版本,它使用 16 位无符号整数作为坐标,或者 `spoint16` 用于带符号 16 位整数。点的任何操作方法,除了以 `_inplace` 结尾的方法,都会返回一个新点。
尺寸
大小表示二维空间中某物的尺寸。它由 `width` 和 `height` 成员组成,此外还有一个成员用于根据大小获取边界矩形,以及用于转换为和从带符号 (`ssize16`) 和无符号 (`size16`) 版本的成员。大小的任何操作方法,除了以 `_inplace` 结尾的方法,都会返回一个新大小。
矩形
矩形是位置功能的主力军。它们由 `x1`、`y1`、`x2` 和 `y2` 表示的两个二维坐标组成。矩形提供了一系列用于检索信息和操作它们的方法,包括居中、膨胀、翻转和归一化等等。您通常会使用 `rect16` 表示无符号版本,或 `srect16` 表示带符号版本。与点和大小一样,所有操作方法,除了以 `_inplace` 结尾的方法,都会返回一个新的矩形。
路径
路径指定了一系列由点表示的连接线段。通过 `begin()`、`operator[]` 和 `size()` 表示,类似于 STL 容器。它们在操作方式上有点异类,因为它们需要传入外部点缓冲区/数组。这样做的原因是为了避免不必要的堆操作。GFX 通常不愿进行隐式堆分配,这也是位图和路径接受指向外部缓冲区的指针而不是创建自己的缓冲区的原因之一。因此,唯一的操作方法是 `_inplace`,并且没有从现有路径自动创建新路径的功能。在这方面,它们与其他定位对象非常不同。通常,要使用一个,您需要创建一组点(如果制作多边形,则按顺时针顺序),然后对其进行 `offset_inplace()` 以将其移动到您需要的位置。您通常会使用 `spath16`,因为绘图操作接受这种类型。
正中靶心
绘图目标是可用于绘图操作的来源或目的地。来源是可读取的事物,目的地是可写入的事物。有些事物两者兼是。绘图目标是设备(例如 LCD 显示屏)或位图之类的东西。所有绘图操作都需要一个作为绘图目的地的绘图目标。有些还需要第二个绘图目标——一个绘图来源。
一幅画!
使用上述工具,我们可以几乎在任何地方定义和绘制点、线条和形状。为此,我们使用 `draw` 类,其形式基本上是 `draw::`**{object}**`(destination,`**{position}**`,`**{pixel/color}**`,`**{other options}...**`)`。
**{object}** 指示我们要绘制的对象类型,例如 `arc` 或 `filled_rectangle`。
**{position}** 是绘制操作的坐标,通常是 `srect16`,但根据 **{object}** 可能是 `spoint16` 或 `spath16`。
**{pixel/color}** 指示绘图操作要使用的颜色。任何格式的像素都将被接受,并进行必要的转换。Alpha 通道会受到尊重,但它们必须在同时作为绘制源的绘制目标上完成——换句话说,一个支持读取的绘制目标。当您需要 Alpha 混合时,建议使用位图作为目标,因为所需的读写次数使其通过 SPI 总线进行操作的速度极其缓慢。因此,不建议直接在显示屏上进行 Alpha 混合。将来,GFX 将自动使用临时中间位图来促进更快的混合,但目前请避免直接在显示屏上进行 Alpha 混合。
**{其他选项}** 是零个或多个参数,并取决于 **{对象}**。
现在来点完全不同的东西
在这里,我将作为一种旁白,介绍双缓冲,因为我们在演示中使用了它。双缓冲可以防止在绘制到显示器时出现“撕裂”现象,并且是进行动画时常用的一种技术。撕裂会导致显示器在绘制动画时出现闪烁。不幸的是,虽然使用双缓冲可以解决这个问题,但它需要保留一个离屏位图,其中包含一帧完整的显示像素。这在 320x240x16bpp 时是 150KB,看起来可能不多,但在一个小小的物联网系统上尝试找到一个如此大小的连续空闲内存块是相当困难的。
为了解决堆上没有连续内存块的问题,我们简单地使用非连续内存,也就是说,我们使用几个内存块并将其呈现为单个位图。这是通过我们在演示中使用的 `large_bitmap<>` 类完成的。我们将其声明为大型位图的每一行都是一个单行大小的普通位图。大型位图管理着 240 个 320x1(在这种情况下)的位图,以提供一个无缝的绘图目标。
双缓冲也为我们解决了另外两个问题。第一个是目前无法从 ILI9341 读取,尽管这将在未来改变。由于无法从它读取,您也无法在其上正确进行 alpha 混合。即使可以,由于它被迫生成的所有总线流量,速度也会非常慢。通过绘制到我们的离屏缓冲区,我们重新获得了 alpha 混合能力,因为位图是可读的。我们还避开了尝试通过总线进行 alpha 混合的性能问题。相反,我们定期将离屏位图发送到屏幕,这相对较快,特别是与替代方案相比。
需要注意的是,虽然 `large_bitmap<>` 在这种情况下提高了性能,但它的性能明显低于原生位图,因为它不能直接进行 blt 操作,也不实现 `copy_from<>()` 或 `copy_to<>()`,至少在当前版本的 GFX 中是这样。即使目标支持异步传输,它也不能异步传输,因为它不是“真正的”位图。尽管如此,在这里使用它仍然能给我们带来很大的好处。
编写这个混乱的程序
如果以上内容看起来很复杂,实际的绘图代码其实非常简单。但是,我们将在这里将其分为 ESP32 特有的部分和 GFX 部分,以便您了解它们之间的关系,以及代码的哪些部分可以直接移植到其他平台。
设置 ILI9341 显示器
ESP32 特有
安装基础
从头开始
#include "spi_master.hpp"
#include "ili9341.hpp"
using namespace espidf;
ILI9341 在 SPI 总线上运行,并且演示配置为使用 HSPI 总线的标准引脚。MOSI 是 23。MISO 是 19。SCLK 是 18。我喜欢使用 `#define` 来使它们易于修改。请注意,无论使用哪个 SPI 设备,此步骤都是必需的。如果您在 HSPI 总线上有多个设备,您只需要这段代码一次,但在任何设备初始化之前
// configure the spi bus. Must be done before the driver
spi_master spi_host(nullptr,
LCD_HOST, // HSPI
PIN_NUM_CLK, // 18
PIN_NUM_MISO, // 19
PIN_NUM_MOSI, // 23
GPIO_NUM_NC, // -1
GPIO_NUM_NC, // -1
4104, // this should be as large as the largest bmp transfer + 8
// for the demo we don't really need it, but the above is reasonable
DMA_CHAN); // 2
我直接把上面的东西复制粘贴到新项目中。对于同一个 HSPI 总线,它总是相同的,除非您使用自定义引脚。
现在我们可以配置驱动程序了。与上述不同,驱动程序的配置是使用模板参数指定的。除非您连接了多个 ILI9341 显示器,否则这实际上更高效。
我们使用的是 CS 引脚 5。其余的,DC 是 2,RST 是 4,背光是引脚 15。
// set up the driver type
using lcd_type = ili9341<LCD_HOST, // HSPI, must match the spi_host init above
PIN_NUM_CS, // 5
PIN_NUM_DC, // 2
PIN_NUM_RST, // 4
PIN_NUM_BCKL>; // 15
// instantiate the driver
lcd_type lcd;
恭喜!您现在已经创建了一个名为 `lcd` 的绘图目标,它代表您的显示器。所有这些都是 ESP32 特有的,实际上不是 GFX 的一部分,尽管驱动程序是 GFX 感知的并且对其有依赖。
GFX 特有
设置表格
我们正在做的其余工作是平台无关的,并且依赖于 GFX 库,而不是任何特定的驱动程序代码。
// ESP32 specific headers and
// namespace from above omitted
// but would be here
...
#include "gfx_cpp14.hpp"
using namespace gfx;
// ESP32 specific initialization code
// from above omitted, but here:
...
现在让我们通过在上面的声明下方声明以下内容来开始使用它
// easy access to lcd color enumeration
using lcd_color = color<typename lcd_type::pixel_type>;
这样做的目的是为了方便为 LCD 选择兼容的颜色。虽然您可以将任何格式的像素传递给绘图函数,但使用原生像素效率更高,因为不需要进行转换。看到我们是如何传入 `lcd_type` 的 `pixel_type` 的吗?这样,颜色枚举将为我们提供以适当格式的像素表示的颜色。所有绘图目标都公开一个 `pixel_type`,它指示它们原生支持的像素格式。
现在我们可以做
typename lcd_type::pixel_type px = lcd_color::yellow; // X11 color
或者更简单,如果您是那些不介意 `auto` 的人之一
auto px = lcd_color::yellow; // X11 color
帧缓冲区登场,左侧
无论如何,现在我们已经介绍了设置头文件、驱动程序和颜色,我们可以配置帧缓冲区了。请记住,大多数情况下您根本不会使用帧缓冲区。它对某些事情很有用,但除非您正在制作游戏之类的东西,否则您真的不想“花费”它所需的巨大内存量。在这里,由于我之前概述的原因,我们确实需要一个,所以让我们设置它
// declare the frame buffer
using fb_type = large_bitmap<typename lcd_type::pixel_type>;
fb_type fb(lcd.dimensions(),1);
与 `lcd_type` 一样,我们也为帧缓冲器声明了一个类型。几乎每次引入一种新的绘图目标类型时,您都会想要为该类型声明一个别名。`large_bitmap<>` 将像素类型作为其唯一的模板参数。像素类型决定了位图的内存布局,也决定了容纳位图所需的内存大小。例如,单色位图将 8 个像素打包成一个字节,但我们的 `lcd_type::pixel_type` 是 16 位 RGB,每个像素需要 2 字节,因此它需要单色位图的 16 倍内存。显然我们想要颜色,在这种情况下,我们希望它与显示器具有相同的颜色模型和分辨率。这就是我们上面使用 `lcd_type::pixel_type` 的原因。
一旦我们声明了类型,我们就创建一个该类型的实例,传入所需的尺寸,这与我们的 `lcd` 相同,以及 `1`,它表示位图每个段中的行数。在这里,我们每个段使用一行,这需要 240 个 320x1x16bpp 的段,这意味着每个段 640 字节。这应该很容易在堆上分配。实际上,当我们初始化它时,它运行良好。如果它没有,它不会抛出异常,因为 gfx 是无异常的,但 `initialized()` 将为 false,任何尝试使用它都会返回 `gfx_result::out_of_memory`。
无论如何,一旦它被创建,我们只需绘制到 `fb` 而不是 `lcd`。当我们完成绘制后,我们用这段代码一次性将整个 `fb` 发送到 `lcd`
// send the frame buffer to the screen
draw::bitmap(lcd,(srect16)lcd.bounds(),fb,fb.bounds());
在那里,您可以看到我们正在将整个 `fb` 绘制到 `lcd`。
请注意,我们必须将 `lcd` 的边界矩形转换为带符号的版本,即 `srect16`。绘图方法接受带符号的值作为其目标定位,以便可以部分绘制到屏幕外。但是,边界矩形和源矩形都没有理由是带符号的。一个简单的类型转换修复了上面的“极性不匹配”问题。
您可能还注意到我们的目的地排在第一位。它以这种方式像 `memcpy()` 一样工作,但它也更有意义,因为所有绘图方法都将绘图目的地作为它们的第一个参数,但并非所有绘图方法都接受绘图源。事实上,大多数都不接受。
从现在开始,在演示中,我们将绘制到 `fb`。
拿出蜡笔
...或者在这种情况下,像素。让我们从一些简单的东西开始
// draw a checkerboard pattern
draw::filled_rectangle(fb,(srect16)fb.bounds(),lcd_color::black);
for(int y = 0;y<lcd.dimensions().height;y+=16) {
for(int x = 0;x<lcd.dimensions().width;x+=16) {
if(0!=((x+y)%32)) {
draw::filled_rectangle(fb,
srect16(
spoint16(x,y),
ssize16(16,16)),
lcd_color::white);
}
}
}
在这里,我们将整个帧缓冲区填充为 `lcd_color::black`。现在您可以看到我们之前的 `color<>` 别名在哪里派上用场了。然后我们循环,每隔 16 个像素块,我们绘制一个 16x16 的白色正方形。
这是一个不错的图案,可以在上面展示 Alpha 混合,但我们稍后会讲到。
...除了吃掉它们,我们还能做些什么
我们来制作一个随机颜色。我们将通过将像素的各个通道设置为随机值来完成此操作。
// fb is 16-bit color with no alpha channel.
// it can still do alpha blending on draw but two
// things must occur: you must use a pixel with
// an alpha channel, and the destination for the
// draw must be able to be read, which bitmaps can.
// create a 32-bit RGB pixel with an alpha channel
rgba_pixel<32> px;
// set the color channels to random values:
px.channel<channel_name::R>(rand()%256);
px.channel<channel_name::G>(rand()%256);
px.channel<channel_name::B>(rand()%256);
// set the alpha channel to a constrained value
// so none are "too transparent"
px.channel<channel_name::A>((256-92)+(rand()%92));
如果我们想完全随机化每个通道,还有另一种稍微快一点的方法
px.native_value = (unsigned int)rand();
无论您做什么,这都会为您提供一些随机的颜色值。
注意我们有那个 alpha 通道,如 `channel_name::A` 所示。事实上,我们有它意味着 alpha 混合正在起作用。现在当我们使用 `px` 时,我们基本上是用半透明的蜡笔绘画,这意味着当我们绘画时,下面的颜色会渗透出来。渗透的量取决于 alpha 通道的值。较高的值更不透明。这只要绘制目标支持读取(换句话说,它也是一个绘制源)就可以工作。`fb` 支持读取,但 `lcd` 不支持。我们正在绘制到 `fb`,所以一切都很好。
我们的大多数用于图元(如 `ellipse<>()`)的绘图函数都接受与 `filled_rectangle<>()` 相同的参数。
演示代码中唯一有趣的是我们正在将其随机化
draw::filled_ellipse(fb,
srect16(
rand()%lcd.dimensions().width,
rand()%lcd.dimensions().height,
rand()%lcd.dimensions().width,
rand()%lcd.dimensions().height),
px);
这将绘制一个填充的椭圆,其位置和边界是随机的,并使用我们之前创建的随机半透明颜色。
由于其他方法相同,我们来看一些更有趣的东西。
几个有效的点组成一个参数多边形
多边形是使用路径来描述的,我们之前简要提到了它。路径,再次强调,是一系列点。作为规则,我们希望以顺时针方向声明我们的多边形点。通常,我们会创建我们的路径,然后通过偏移来移动整个路径,直到它处于正确的位置
const float scalex = (rand()%101)/100.0;
const float scaley = (rand()%101)/100.0;
const uint16_t w = lcd.dimensions().width,
h = lcd.dimensions().height;
spoint16 path_points[] = {
// lower left corner
spoint16(0,scaley*h),
// upper middle corner
spoint16(scalex*w/2,0),
// lower right corner
spoint16(scalex*w,scaley*h)
// the final point is implicit
// as polygons automatically
// connect the final point to
// the first point
};
spath16 poly_path(3,path_points);
// position it on the screen
poly_path.offset_inplace(rand()%w,rand()%h);
// now draw it
draw::filled_polygon(fb,poly_path,px);
在这里,我们正在绘制一个占据整个屏幕的三角形,但我们按随机量将其缩小。然后我们使用 `offset_inplace()` 按随机量定位三角形在屏幕上的某个位置。请注意,我们可能应该允许它变为负值,以便它可以绘制到顶部和左侧屏幕外,但为了演示,我想尽可能保持简单。
减少、重用和回收你的代码
您可能希望创建一些为您执行绘图的方法。在演示中,我们有 `draw_happy_face()`,它出乎意料地绘制了一个笑脸
template<typename Destination>
void draw_happy_face(Destination& bmp,float alpha,const srect16& bounds)
注意它是一个模板函数。我们在演示中并不严格需要它。我们可以像这样声明它
void draw_happy_face(large_bitmap<typename lcd_type::pixel_type>& bmp ...
但是,如果那样做,我们就无法将 `lcd` 传递给它。它将只接受一种绘图目标类型——一个带有 16 位 RGB 颜色的大位图。声明一个接受 `Destination` 的模板,然后将 `Destination& destination` 作为参数,将使您的例程适用于任何绘图目标。同样,如果您需要接受绘图源,请对 `Source` 参数执行相同的操作。
总结
演示代码展示了上面概述的技术。您会注意到演示的帧速率非常糟糕。这是 Alpha 混合的一部分。每个像素都必须读取、混合,然后重写,从而使任何批量像素移动代码失效,并退回到读取和写入绘图目标的最慢方式。如果没有某种硬件加速,几乎无能为力。可以使用 SIMD 指令在源上进行混合,但此库不支持,因为所有这些都非常依赖平台。
尽管速度很慢,但概念是存在的,您会注意到,如果移除 Alpha 混合,速度会快得多。
希望这能让您更好地了解如何使用 GFX。祝您愉快!
历史
- 2021 年 6 月 3 日 - 初次提交