加入离线 sprite 与位图字体转换流程
Demo 现在通过生成的 C++ 头文件使用测试 sprite 和像素风 bitmap font,避免在运行时解码 PNG/TTF。同步补充资源转换文档,明确源资源、生成脚本和生成头文件需要一起提交,保证后续可追溯和可重新生成。 Constraint: IMX6U 运行时路径应尽量轻量,避免图片/字体解码开销 Constraint: 生成像素沿用当前 framebuffer 的 uint32_t RGBA 格式 Rejected: 运行时加载 PNG/TTF | 会增加解码依赖和运行时成本 Confidence: high Scope-risk: moderate Directive: 修改 sprite 或字体时,必须同步提交源资源、转换脚本和生成头文件 Tested: cmake -B build-win .; cmake --build build-win --config Release Not-tested: 尚未在 IMX6U 真机上验证 ARM framebuffer/SDL 后端
This commit is contained in:
parent
3e735e27b0
commit
213fa7e961
|
|
@ -59,3 +59,5 @@ build-win
|
|||
build-linux
|
||||
|
||||
.idea
|
||||
|
||||
assets/test
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ target_include_directories(IMX6U-Game PRIVATE
|
|||
src/Gfx/RenderData
|
||||
src/Gfx/Scene
|
||||
src/Gfx/Shading
|
||||
assets/font
|
||||
assets/sprite
|
||||
)
|
||||
|
||||
if(USE_FRAMEBUFFER)
|
||||
|
|
|
|||
69
README.md
69
README.md
|
|
@ -57,7 +57,7 @@ cd IMX6U-Game
|
|||
仓库已自带 SDL2 开发库(`libs/Win/SDL2`),无需额外安装。
|
||||
|
||||
```bash
|
||||
cmake -B build-win .m
|
||||
cmake -B build-win .
|
||||
cmake --build build-win --config Release
|
||||
```
|
||||
|
||||
|
|
@ -128,6 +128,61 @@ scp build-arm-fb/IMX6U-Game root@imx6u:/tmp/
|
|||
|
||||
Framebuffer 对照版本可能需要 root 权限访问 `/dev/fb0`。SDL2 版本是否需要额外环境变量或权限,取决于目标板 SDL2 视频驱动和显示栈配置。
|
||||
|
||||
## 资源转换工具
|
||||
|
||||
项目运行时不依赖 PNG/TTF 解码库。图片和字体资源在离线阶段转换成 C++ 头文件,运行时直接以 `uint32_t` 数组访问,像素格式统一为:
|
||||
|
||||
```text
|
||||
(R << 24) | (G << 16) | (B << 8) | A
|
||||
```
|
||||
|
||||
当前转换工具位于 `tools/`,需要 Python 和 Pillow:
|
||||
|
||||
```bash
|
||||
pip install pillow
|
||||
```
|
||||
|
||||
### Sprite 转换
|
||||
|
||||
普通 PNG sprite 使用 `tools/png_to_header.py` 转换:
|
||||
|
||||
```bash
|
||||
python tools/png_to_header.py assets/sprite/test_sprite.png assets/sprite/test_sprite.h test_sprite
|
||||
```
|
||||
|
||||
生成的头文件会包含:
|
||||
|
||||
```cpp
|
||||
test_sprite_width
|
||||
test_sprite_height
|
||||
test_sprite_pixels
|
||||
```
|
||||
|
||||
透明像素仍保留 alpha;当前 demo 通过 `RenderData::Image(..., 0x00000000)` 把全透明像素作为 color key 跳过。
|
||||
|
||||
### Bitmap Font 转换
|
||||
|
||||
像素字体图集使用 `tools/gen_font_atlas.py` 生成:
|
||||
|
||||
```bash
|
||||
python tools/gen_font_atlas.py assets/font
|
||||
```
|
||||
|
||||
脚本默认优先读取:
|
||||
|
||||
```text
|
||||
assets/font/ndrtyyl-prqyys-undertale-hebrew-uppercase.ttf
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```text
|
||||
assets/font/font_atlas.png
|
||||
assets/font/font_atlas.h
|
||||
```
|
||||
|
||||
字体范围为 ASCII 32~126,按 16 列排列。生成端会把字体 alpha 阈值化为 0/255,以匹配当前 `DrawContext::draw_text` 的像素风路径;运行时只把非透明像素替换成调用方指定颜色。
|
||||
|
||||
## 显示后端架构
|
||||
|
||||
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Gfx` 的平台适配层:
|
||||
|
|
@ -158,6 +213,12 @@ IMX6U-Game/
|
|||
│ └─ Win/
|
||||
│ ├─ SDL2/ # Windows 用 SDL2 库(头文件 + lib + DLL)
|
||||
│ └─ SDL_image/ # SDL2_image 库(头文件 + lib + DLL)
|
||||
├─ assets/
|
||||
│ ├─ font/ # 像素字体源文件、font_atlas.png、font_atlas.h
|
||||
│ └─ sprite/ # PNG sprite 源文件及转换后的头文件
|
||||
├─ tools/
|
||||
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
|
||||
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
|
||||
├─ src/
|
||||
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
|
||||
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口
|
||||
|
|
@ -183,7 +244,7 @@ IMX6U-Game/
|
|||
## 模块说明
|
||||
|
||||
### Draw2D
|
||||
- **DrawContext**:统一绘制入口,封装 FrameBuffer、DepthBuffer、Rasterizer、TriangleRasterizer,对外提供 `clear`、`draw_line`、`draw_triangle`、`present` 接口
|
||||
- **DrawContext**:统一绘制入口,封装 FrameBuffer、DepthBuffer、Rasterizer、TriangleRasterizer,对外提供 `clear`、`draw_line`、`draw_triangle`、`draw_sprite`、`draw_text`、`present` 接口
|
||||
|
||||
### Core
|
||||
- **FrameBuffer**:CPU 侧颜色缓冲,渲染结果先写在这里
|
||||
|
|
@ -207,6 +268,8 @@ IMX6U-Game/
|
|||
**已完成:**
|
||||
- 可旋转立方体的 3D 渲染(MVP 变换、背面剔除、扫描线填充、深度测试)
|
||||
- 双平台显示后端(SDL2 / Framebuffer)
|
||||
- 离线资源转换工具:PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
|
||||
- 基础 2D sprite 与 bitmap font 文本绘制,当前 demo 显示 FPS 文本和测试 sprite
|
||||
- Gfx 目录规范化,代码收敛到 `src/Gfx/`
|
||||
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
|
||||
- C++11 兼容代码
|
||||
|
|
@ -216,7 +279,7 @@ IMX6U-Game/
|
|||
1. FrameBuffer 性能优化(`memset` 清屏、去掉 `at()`、定点数/NEON)
|
||||
2. 应用层拆分(Launcher / GameA / GameB / Shared)和统一 `IApp` 主循环
|
||||
3. SDL2 输入抽象(键盘/触摸/按键状态快照)
|
||||
4. Gfx 基础 2D 绘制接口(矩形、四边形、Sprite、Tilemap)
|
||||
4. Gfx 基础 2D 绘制接口(矩形、四边形、Tilemap,继续完善 Sprite/Text 的裁剪和批处理)
|
||||
5. 纹理贴图、OBJ 模型加载与完整光照
|
||||
|
||||
## 说明
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
|
@ -0,0 +1,25 @@
|
|||
// Auto-generated by tools/png_to_header.py
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
static const int32_t test_sprite_width = 16;
|
||||
static const int32_t test_sprite_height = 16;
|
||||
|
||||
static const uint32_t test_sprite_pixels[] = {
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xFF6464FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0xDC2828FF, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 122 B |
|
|
@ -29,10 +29,15 @@ IMX6U-Game/
|
|||
│ ├─ UI/ # 启动器和游戏共用 UI 组件
|
||||
│ └─ Assets/ # 应用层资源索引、资源命名约定
|
||||
├─ assets/
|
||||
│ ├─ font/ # 共享像素字体源文件与生成的 font_atlas
|
||||
│ ├─ sprite/ # 当前 demo/test sprite 源文件与生成头文件
|
||||
│ ├─ launcher/
|
||||
│ ├─ game_a/
|
||||
│ ├─ game_b/
|
||||
│ └─ shared/
|
||||
├─ tools/
|
||||
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
|
||||
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
|
||||
└─ docs/
|
||||
```
|
||||
|
||||
|
|
@ -50,6 +55,7 @@ IMX6U-Game/
|
|||
- 提供基础绘制接口:点、线、矩形、四边形、三角形、sprite、tile、简单文本等。
|
||||
- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。
|
||||
- 封装 SDL2 / framebuffer 显示提交、输入轮询、时间源。
|
||||
- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas),运行时不直接依赖 PNG/TTF 解码。
|
||||
- 只关心“怎么画得快、怎么提交到屏幕”,不关心具体游戏规则。
|
||||
|
||||
禁止:
|
||||
|
|
@ -58,6 +64,7 @@ IMX6U-Game/
|
|||
- 不包含具体关卡、角色、菜单流程、游戏状态机。
|
||||
- 不直接读取某个游戏专属资源路径。
|
||||
- 不在核心绘制接口中暴露 SDL2 类型。
|
||||
- 不在每帧热路径中执行图片/字体解码;资源转换应在构建前或工具阶段完成。
|
||||
|
||||
### 2.2 Apps/GameA 与 Apps/GameB:两个游戏
|
||||
|
||||
|
|
@ -178,16 +185,17 @@ namespace Gfx
|
|||
void fill_rect(RectI rect, Color32 color);
|
||||
void draw_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
|
||||
void fill_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
|
||||
void draw_sprite(int32_t x, int32_t y, const RenderData::Image& image);
|
||||
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
||||
RenderData::Color color, const char* text);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
后续再扩展:
|
||||
|
||||
- `draw_sprite`
|
||||
- `draw_sprite_region`
|
||||
- `draw_tilemap`
|
||||
- `draw_text_bitmap_font`
|
||||
- `set_clip_rect`
|
||||
- `set_camera_2d`
|
||||
- `batch sprite/tile`
|
||||
|
|
@ -210,7 +218,7 @@ namespace Gfx
|
|||
|
||||
1. ~~先抽出统一 `IApp` 和 `AppManager`,让当前 demo 成为一个 app。~~
|
||||
2. ~~把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。~~
|
||||
3. ~~建立 `Gfx::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**(`Gfx::DrawContext` 封装了 clear、draw_line、draw_triangle、present)
|
||||
3. ~~建立 `Gfx::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**(`Gfx::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、draw_text、present)
|
||||
4. ~~底层代码迁移到 `src/Gfx/`,Demo 入口迁移到 `src/Apps/Demo/`。~~ **已完成**
|
||||
5. 新增 Launcher app,只做最小菜单和应用切换。
|
||||
6. 新增 GameA/GameB 空壳,验证三应用切换。
|
||||
|
|
@ -225,3 +233,11 @@ namespace Gfx
|
|||
- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。
|
||||
- Gfx 的绘制函数要保持小而直接,优先内联和连续内存写入。
|
||||
- UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。
|
||||
|
||||
## 9. 资源转换约定
|
||||
|
||||
- 运行时资源优先使用离线转换后的简单数组,不在 IMX6U 运行时解码 PNG/TTF。
|
||||
- `tools/png_to_header.py` 将 PNG 转为 `uint32_t` RGBA 数组,适用于 sprite、小图标、测试纹理等。
|
||||
- `tools/gen_font_atlas.py` 将共享像素字体转为 ASCII bitmap atlas,并输出同名 PNG 预览和 C++ 头文件。
|
||||
- 生成头文件、源 PNG/TTF 和转换脚本应一起纳入仓库,保证资源可追溯、可再生成。
|
||||
- 生成数据目前面向简单直接的调试/小型游戏资源;后续如果资源体积增长,应再评估 1-bit/8-bit mask、RLE 或自定义资源包格式。
|
||||
|
|
|
|||
|
|
@ -113,7 +113,18 @@
|
|||
- 断言用于捕捉开发期错误,但不能替代运行时边界处理。
|
||||
- Debug 检查和 Release 快路径应可区分;不要为了调试便利让最终路径长期承担检查成本。
|
||||
|
||||
## 8. 新代码提交前检查清单
|
||||
## 8. 资源转换与运行时资源规范
|
||||
|
||||
- 图片、字体等外部资源应在离线阶段转换为运行时直接可用的数据格式,避免在 IMX6U 热路径或启动关键路径中引入 PNG/TTF 解码成本。
|
||||
- 当前工具约定:
|
||||
- `tools/png_to_header.py`:PNG -> `uint32_t` RGBA 头文件。
|
||||
- `tools/gen_font_atlas.py`:像素字体 TTF -> ASCII bitmap font atlas/header。
|
||||
- 生成像素格式统一为 `(R << 24) | (G << 16) | (B << 8) | A`,与 `RenderData::Color::to_rgba()` 和 `Core::FrameBuffer` 当前格式保持一致。
|
||||
- 源资源、生成脚本和生成头文件应同时提交,保证资源可追溯、可复现。
|
||||
- 运行时绘制 sprite/font 时只做裁剪、透明判断、颜色替换或必要的像素拷贝;不要在绘制函数内做文件 IO、图片解码、字体栅格化或动态分配。
|
||||
- 字体资源当前走像素风路径,生成端会把 alpha 阈值化为 0/255;如果未来要恢复抗锯齿字体,必须同步设计 framebuffer alpha blending,而不能只在绘制端把所有非 0 alpha 当作实心像素。
|
||||
|
||||
## 9. 新代码提交前检查清单
|
||||
|
||||
新增或修改核心代码前,至少检查:
|
||||
|
||||
|
|
@ -123,10 +134,11 @@
|
|||
- [ ] 是否有隐藏的临时大对象拷贝?
|
||||
- [ ] 是否把平台/显示层 API 类型泄漏进核心逻辑?
|
||||
- [ ] 是否能在 PC 调试版和 ARM release 版分别关闭调试开销?
|
||||
- [ ] 新增图片/字体资源是否已离线转换,且源资源、转换脚本、生成头文件一起提交?
|
||||
- [ ] 是否保留 C++11 兼容?
|
||||
- [ ] 是否需要同步更新 `docs/CONVENTIONS.md` 中的坐标/矩阵/深度等约定?
|
||||
|
||||
## 9. 推荐的代码结构方向
|
||||
## 10. 推荐的代码结构方向
|
||||
|
||||
后续如果继续推进性能优化,优先建立这些基础设施:
|
||||
|
||||
|
|
@ -136,7 +148,7 @@
|
|||
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
|
||||
5. 简单基准测试或计时工具,用于比较清屏、光栅化、深度测试等关键路径。
|
||||
|
||||
## 10. 与现有代码的关系
|
||||
## 11. 与现有代码的关系
|
||||
|
||||
当前项目仍有历史浮点实现(如数学、相机、深度缓冲等)。本文档不是要求一次性重写,而是作为后续开发红线:
|
||||
|
||||
|
|
@ -144,18 +156,18 @@
|
|||
- 修改已有热路径时,优先向定点、复用缓冲、连续内存方向收敛。
|
||||
- 每次性能相关重构都应保持行为可验证,避免一次性大改导致渲染结果难以回归。
|
||||
|
||||
## 11. IMX6U + SDL2 运行后端规范
|
||||
## 12. IMX6U + SDL2 运行后端规范
|
||||
|
||||
如果最终版本在 IMX6U 上使用 SDL2,而不是直接写 framebuffer,需要额外注意:SDL2 只是显示、输入、计时和窗口/屏幕适配层,不能把 SDL 渲染 API 当成主要性能来源。项目仍然应以 CPU 侧 framebuffer 为核心渲染结果,再以最少拷贝提交给 SDL2。
|
||||
|
||||
### 11.1 SDL2 边界
|
||||
### 12.1 SDL2 边界
|
||||
|
||||
- SDL2 类型和调用只允许出现在 `src/Gfx/Platform/` 以及明确的平台适配层中。
|
||||
- `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/Rasterizer`、`src/Gfx/RenderData`、`src/Gfx/Scene`、`src/Gfx/Draw2D` 不应直接包含 `SDL.h`。
|
||||
- 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。
|
||||
- 时间源可以来自 SDL,但核心逻辑使用整数 tick / fixed timestep,不直接依赖 float 秒数。
|
||||
|
||||
### 11.2 提交帧策略
|
||||
### 12.2 提交帧策略
|
||||
|
||||
- 每帧只提交一次最终 framebuffer,避免在一帧内多次 `SDL_RenderPresent()`。
|
||||
- 优先使用固定尺寸 streaming texture;初始化时创建,运行时复用。
|
||||
|
|
@ -164,14 +176,14 @@
|
|||
- 如果使用 `SDL_UpdateTexture` 成为瓶颈,优先评估 `SDL_LockTexture` 直接写入 texture 缓冲,减少一次额外拷贝。
|
||||
- 只更新 dirty rect 的策略可以用于 2D UI/地图类画面;但全屏软光栅 3D 通常仍是整帧提交,重点在降低 CPU 侧绘制成本。
|
||||
|
||||
### 11.3 SDL Renderer 选择
|
||||
### 12.3 SDL Renderer 选择
|
||||
|
||||
- IMX6U 上不要假设 `SDL_RENDERER_ACCELERATED` 一定更快;实际可能走软件或受驱动限制。
|
||||
- 需要在开发板上对比 `SDL_RENDERER_ACCELERATED`、`SDL_RENDERER_SOFTWARE`、默认 renderer 的帧时间和稳定性。
|
||||
- 一旦确定目标板最快/最稳配置,应在代码或 CMake 选项中固定,不要依赖默认行为。
|
||||
- 如果 SDL2 后端只是搬运 CPU framebuffer,重点关注 texture update、copy、present 的总耗时,而不是复杂 SDL 绘图 API。
|
||||
|
||||
### 11.4 分辨率与帧率预算
|
||||
### 12.4 分辨率与帧率预算
|
||||
|
||||
- 先确定目标分辨率和目标 FPS,再决定渲染功能;不要默认使用屏幕原生高分辨率。
|
||||
- IMX6U 上优先考虑低分辨率内部渲染,再由 SDL/显示层整数倍放大。
|
||||
|
|
@ -179,14 +191,14 @@
|
|||
- 性能档应限制:最大三角形数、最大 sprite 数、最大粒子数、最大动态光源数、最大纹理尺寸。
|
||||
- 所有预算都应以实测帧时间为准,不能只按 PC 表现推断。
|
||||
|
||||
### 11.5 输入、音频和资源
|
||||
### 12.5 输入、音频和资源
|
||||
|
||||
- SDL 输入事件应每帧集中轮询一次,转换为当前帧输入快照;不要在逻辑各处直接轮询 SDL。
|
||||
- 音频回调中禁止分配内存、加锁等待或做复杂逻辑。
|
||||
- 图片、音频、地图等资源必须在加载阶段解码;运行中避免临时解码和格式转换。
|
||||
- 大纹理/图片进入运行时前应转换成目标像素格式和目标尺寸。
|
||||
|
||||
### 11.6 必须建立的性能观测
|
||||
### 12.6 必须建立的性能观测
|
||||
|
||||
在 IMX6U 上接入 SDL2 后,应尽快加入轻量 profiler 或计时日志,至少拆分统计:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include <iostream>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include "Vector2.h"
|
||||
#include "Vector3.h"
|
||||
#include "Vector4.h"
|
||||
|
|
@ -12,6 +13,8 @@
|
|||
#include <cstdlib>
|
||||
#include "Vertex.h"
|
||||
#include "DrawContext.h"
|
||||
#include "test_sprite.h"
|
||||
#include "font_atlas.h"
|
||||
|
||||
#include "Display.h"
|
||||
#ifdef USE_FRAMEBUFFER
|
||||
|
|
@ -116,6 +119,13 @@ int main(int argc, char *argv[])
|
|||
|
||||
Gfx::DrawContext ctx(width, height);
|
||||
|
||||
RenderData::BitmapFont font;
|
||||
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
|
||||
font.char_w = font_char_w;
|
||||
font.char_h = font_char_h;
|
||||
font.columns = font_columns;
|
||||
font.first_char = font_first_char;
|
||||
|
||||
Scene::Camera camera;
|
||||
camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f);
|
||||
camera.transform.rotation = Math::Vector3(0.0f, 3.1415926535f, 0.0f);
|
||||
|
|
@ -147,8 +157,15 @@ int main(int argc, char *argv[])
|
|||
|
||||
const RenderData::Color clearColor(18, 18, 24, 255);
|
||||
const RenderData::Color cubeColor(240, 240, 240, 255);
|
||||
const RenderData::Color fpsColor(0, 255, 80, 255);
|
||||
const RenderData::Color fpsBg(0, 0, 0, 200);
|
||||
const float aspectRatio = static_cast<float>(width) / static_cast<float>(height);
|
||||
|
||||
int32_t fps = 0;
|
||||
int32_t frame_count = 0;
|
||||
uint32_t last_fps_time = 0;
|
||||
char fps_text[32];
|
||||
|
||||
bool isRunning = true;
|
||||
while (isRunning)
|
||||
{
|
||||
|
|
@ -235,6 +252,25 @@ int main(int argc, char *argv[])
|
|||
}
|
||||
}
|
||||
|
||||
// sprite 测试
|
||||
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
|
||||
|
||||
ctx.draw_sprite(10, 10, sprite_img);
|
||||
ctx.draw_sprite_ex(30, 10, sprite_img, 0, 0, sprite_img.width, sprite_img.height, 2, false, false);
|
||||
ctx.draw_sprite_ex(10, 30, sprite_img, 0, 0, sprite_img.width, sprite_img.height, 3, true, false);
|
||||
|
||||
// FPS 计数
|
||||
++frame_count;
|
||||
const uint32_t now = display->get_time_ms();
|
||||
if (now - last_fps_time >= 1000)
|
||||
{
|
||||
fps = frame_count;
|
||||
frame_count = 0;
|
||||
last_fps_time = now;
|
||||
}
|
||||
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
|
||||
ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text);
|
||||
|
||||
ctx.present(display);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "Rasterizer.h"
|
||||
#include "TriangleRasterizer.h"
|
||||
#include "Display.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace Gfx
|
||||
{
|
||||
|
|
@ -54,6 +55,122 @@ namespace Gfx
|
|||
triangleRasterizer->DrawTriangle2D(triangle, color);
|
||||
}
|
||||
|
||||
void DrawContext::draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::Image& img)
|
||||
{
|
||||
draw_sprite_region(dst_x, dst_y, img, 0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
void DrawContext::draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
|
||||
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h)
|
||||
{
|
||||
draw_sprite_ex(dst_x, dst_y, img, src_x, src_y, src_w, src_h, 1, false, false);
|
||||
}
|
||||
|
||||
void DrawContext::draw_sprite_ex(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
|
||||
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h,
|
||||
int32_t scale, bool flip_h, bool flip_v)
|
||||
{
|
||||
if (scale < 1 || !img.pixels) return;
|
||||
|
||||
const int32_t img_w = img.width;
|
||||
const uint32_t* src = img.pixels;
|
||||
const bool has_key = img.has_color_key;
|
||||
const uint32_t key = img.color_key;
|
||||
|
||||
for (int32_t sy = 0; sy < src_h; ++sy)
|
||||
{
|
||||
const int32_t read_y = flip_v ? (src_y + src_h - 1 - sy) : (src_y + sy);
|
||||
const uint32_t* row = src + read_y * img_w;
|
||||
|
||||
for (int32_t sx = 0; sx < src_w; ++sx)
|
||||
{
|
||||
const int32_t read_x = flip_h ? (src_x + src_w - 1 - sx) : (src_x + sx);
|
||||
const uint32_t pixel = row[read_x];
|
||||
|
||||
if (has_key && pixel == key) continue;
|
||||
|
||||
const int32_t base_x = dst_x + sx * scale;
|
||||
const int32_t base_y = dst_y + sy * scale;
|
||||
|
||||
if (scale == 1)
|
||||
{
|
||||
frameBuffer->set_pixel(base_x, base_y, pixel);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int32_t dy = 0; dy < scale; ++dy)
|
||||
{
|
||||
for (int32_t dx = 0; dx < scale; ++dx)
|
||||
{
|
||||
frameBuffer->set_pixel(base_x + dx, base_y + dy, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawContext::fill_rect(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color)
|
||||
{
|
||||
const uint32_t rgba = color.to_rgba();
|
||||
for (int32_t row = y; row < y + h; ++row)
|
||||
{
|
||||
for (int32_t col = x; col < x + w; ++col)
|
||||
{
|
||||
frameBuffer->set_pixel(col, row, rgba);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawContext::draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
||||
const RenderData::Color& color, const char* text)
|
||||
{
|
||||
if (!font.atlas.pixels || !text) return;
|
||||
|
||||
const uint32_t rgba = color.to_rgba();
|
||||
const int32_t cw = font.char_w;
|
||||
const int32_t ch = font.char_h;
|
||||
const int32_t atlas_w = font.atlas.width;
|
||||
const uint32_t* src = font.atlas.pixels;
|
||||
|
||||
int32_t cursor_x = x;
|
||||
for (const char* p = text; *p; ++p)
|
||||
{
|
||||
const int32_t index = static_cast<int32_t>(*p) - font.first_char;
|
||||
if (index < 0) { cursor_x += cw; continue; }
|
||||
|
||||
const int32_t col = index % font.columns;
|
||||
const int32_t row = index / font.columns;
|
||||
const int32_t src_x = col * cw;
|
||||
const int32_t src_y = row * ch;
|
||||
|
||||
for (int32_t sy = 0; sy < ch; ++sy)
|
||||
{
|
||||
const uint32_t* atlas_row = src + (src_y + sy) * atlas_w;
|
||||
const int32_t dst_y_abs = y + sy;
|
||||
for (int32_t sx = 0; sx < cw; ++sx)
|
||||
{
|
||||
const uint32_t pixel = atlas_row[src_x + sx];
|
||||
if ((pixel & 0xFF) == 0) continue;
|
||||
frameBuffer->set_pixel(cursor_x + sx, dst_y_abs, rgba);
|
||||
}
|
||||
}
|
||||
cursor_x += cw;
|
||||
}
|
||||
}
|
||||
|
||||
void DrawContext::draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
||||
const RenderData::Color& color, const RenderData::Color& bg_color, const char* text)
|
||||
{
|
||||
if (!text) return;
|
||||
|
||||
int32_t len = 0;
|
||||
for (const char* p = text; *p; ++p) ++len;
|
||||
|
||||
fill_rect(x, y, len * font.char_w, font.char_h, bg_color);
|
||||
draw_text(font, x, y, color, text);
|
||||
}
|
||||
|
||||
void DrawContext::present(Platform::IDisplay* display)
|
||||
{
|
||||
display->present(frameBuffer);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#include "Color.h"
|
||||
#include "Vector2.h"
|
||||
#include "Triangle.h"
|
||||
#include "Image.h"
|
||||
#include "BitmapFont.h"
|
||||
#include <cstdint>
|
||||
|
||||
namespace Core
|
||||
|
|
@ -47,6 +49,19 @@ namespace Gfx
|
|||
void draw_line(const Math::Vector2Int& from, const Math::Vector2Int& to, const RenderData::Color& color);
|
||||
void draw_triangle(const RenderData::Triangle& triangle, const RenderData::Color& color);
|
||||
|
||||
void draw_sprite(int32_t dst_x, int32_t dst_y, const RenderData::Image& img);
|
||||
void draw_sprite_region(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
|
||||
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h);
|
||||
void draw_sprite_ex(int32_t dst_x, int32_t dst_y, const RenderData::Image& img,
|
||||
int32_t src_x, int32_t src_y, int32_t src_w, int32_t src_h,
|
||||
int32_t scale, bool flip_h, bool flip_v);
|
||||
|
||||
void fill_rect(int32_t x, int32_t y, int32_t w, int32_t h, const RenderData::Color& color);
|
||||
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
||||
const RenderData::Color& color, const char* text);
|
||||
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
|
||||
const RenderData::Color& color, const RenderData::Color& bg_color, const char* text);
|
||||
|
||||
void present(Platform::IDisplay* display);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include "Image.h"
|
||||
|
||||
namespace RenderData
|
||||
{
|
||||
struct BitmapFont
|
||||
{
|
||||
Image atlas;
|
||||
int32_t char_w;
|
||||
int32_t char_h;
|
||||
int32_t columns;
|
||||
int32_t first_char;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include "Color.h"
|
||||
|
||||
namespace RenderData
|
||||
{
|
||||
struct Image
|
||||
{
|
||||
const uint32_t* pixels;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
uint32_t color_key;
|
||||
bool has_color_key;
|
||||
|
||||
Image() : pixels(nullptr), width(0), height(0), color_key(0), has_color_key(false) {}
|
||||
|
||||
Image(const uint32_t* pixels, int32_t width, int32_t height)
|
||||
: pixels(pixels), width(width), height(height), color_key(0), has_color_key(false) {}
|
||||
|
||||
Image(const uint32_t* pixels, int32_t width, int32_t height, uint32_t color_key)
|
||||
: pixels(pixels), width(width), height(height), color_key(color_key), has_color_key(true) {}
|
||||
|
||||
uint32_t get_pixel(int32_t x, int32_t y) const
|
||||
{
|
||||
return pixels[y * width + x];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
"""
|
||||
用 PIL 渲染 ASCII 像素风位图字体图集。
|
||||
|
||||
用法:
|
||||
python tools/gen_font_atlas.py [output_dir]
|
||||
|
||||
默认输出目录: assets/font/
|
||||
生成: font_atlas.png, font_atlas.h
|
||||
|
||||
字符范围: ASCII 32~126
|
||||
字体: 优先使用 assets/font/ndrtyyl-prqyys-undertale-hebrew-uppercase.ttf,
|
||||
不存在时回退到系统等宽字体(Courier New / Consolas / monospace)
|
||||
|
||||
像素格式: (R << 24) | (G << 16) | (B << 8) | A
|
||||
生成端会把 alpha 阈值化为 0/255,匹配当前 DrawContext::draw_text 的像素风绘制路径。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
FIRST_CHAR = 32
|
||||
LAST_CHAR = 126
|
||||
NUM_CHARS = LAST_CHAR - FIRST_CHAR + 1 # 95
|
||||
COLUMNS = 16
|
||||
ROWS = (NUM_CHARS + COLUMNS - 1) // COLUMNS # 6
|
||||
|
||||
|
||||
def find_mono_font(size: int) -> ImageFont.FreeTypeFont:
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
candidates = [
|
||||
os.path.join(repo_root, "assets/font", "ndrtyyl-prqyys-undertale-hebrew-uppercase.ttf"),
|
||||
"C:/Windows/Fonts/consola.ttf", # Consolas (Windows)
|
||||
"C:/Windows/Fonts/cour.ttf", # Courier New (Windows)
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", # Linux
|
||||
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.exists(path):
|
||||
return ImageFont.truetype(path, size)
|
||||
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def generate(output_dir: str):
|
||||
font = find_mono_font(14)
|
||||
|
||||
# 测量完整 ASCII 字符集的尺寸。
|
||||
# 不能只用 "M":它没有下伸部,按它算高度会裁掉 g/j/p/q/y/_ 等字符。
|
||||
tmp_img = Image.new("RGBA", (1, 1))
|
||||
tmp_draw = ImageDraw.Draw(tmp_img)
|
||||
chars = [chr(FIRST_CHAR + i) for i in range(NUM_CHARS)]
|
||||
bboxes = [tmp_draw.textbbox((0, 0), ch, font=font) for ch in chars]
|
||||
|
||||
pad = 1
|
||||
min_left = min(b[0] for b in bboxes)
|
||||
min_top = min(b[1] for b in bboxes)
|
||||
max_right = max(b[2] for b in bboxes)
|
||||
max_bottom = max(b[3] for b in bboxes)
|
||||
|
||||
# 等宽字体仍使用统一 cell。宽度用字体 advance,避免窄字符影响字距;
|
||||
# 高度用全字符 bbox,保证下伸字符不被裁切。
|
||||
max_advance = max(tmp_draw.textlength(ch, font=font) for ch in chars)
|
||||
char_w = int(max_advance + 0.9999) + pad * 2
|
||||
char_h = (max_bottom - min_top) + pad * 2
|
||||
|
||||
atlas_w = COLUMNS * char_w
|
||||
atlas_h = ROWS * char_h
|
||||
|
||||
img = Image.new("RGBA", (atlas_w, atlas_h), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
for i in range(NUM_CHARS):
|
||||
ch = chr(FIRST_CHAR + i)
|
||||
col = i % COLUMNS
|
||||
row = i // COLUMNS
|
||||
x = col * char_w + pad - min_left
|
||||
y = row * char_h + pad - min_top
|
||||
draw.text((x, y), ch, fill=(255, 255, 255, 255), font=font)
|
||||
|
||||
# 像素风:把 TrueType 抗锯齿 alpha 二值化。
|
||||
# 渲染端目前只判断 alpha 是否非 0;如果保留半透明边缘,会被画成实心像素导致发胖/毛边。
|
||||
threshold = 128
|
||||
for py in range(atlas_h):
|
||||
for px in range(atlas_w):
|
||||
_, _, _, a = img.getpixel((px, py))
|
||||
img.putpixel((px, py), (255, 255, 255, 255) if a >= threshold else (0, 0, 0, 0))
|
||||
|
||||
png_path = os.path.join(output_dir, "font_atlas.png")
|
||||
img.save(png_path)
|
||||
|
||||
# 生成 C 头文件:将每行像素打包成 bit 行(每像素 1 bit,白色=1,透明=0)
|
||||
# 这样每个 glyph 的一行只需要 char_w/8 个字节
|
||||
# 但为了简单,这里直接存储为 uint32_t RGBA 像素数组
|
||||
# 使用时白色像素用指定颜色替换,透明像素跳过
|
||||
|
||||
pixels = []
|
||||
for y in range(atlas_h):
|
||||
for x in range(atlas_w):
|
||||
r, g, b, a = img.getpixel((x, y))
|
||||
pixels.append((r << 24) | (g << 16) | (b << 8) | a)
|
||||
|
||||
h_path = os.path.join(output_dir, "font_atlas.h")
|
||||
with open(h_path, "w", encoding="utf-8") as f:
|
||||
f.write("// Auto-generated by tools/gen_font_atlas.py\n")
|
||||
f.write("#pragma once\n#include <cstdint>\n\n")
|
||||
f.write(f"static const int32_t font_atlas_width = {atlas_w};\n")
|
||||
f.write(f"static const int32_t font_atlas_height = {atlas_h};\n")
|
||||
f.write(f"static const int32_t font_char_w = {char_w};\n")
|
||||
f.write(f"static const int32_t font_char_h = {char_h};\n")
|
||||
f.write(f"static const int32_t font_first_char = {FIRST_CHAR};\n")
|
||||
f.write(f"static const int32_t font_columns = {COLUMNS};\n\n")
|
||||
f.write(f"static const uint32_t font_atlas_pixels[] = {{\n")
|
||||
|
||||
for i in range(0, len(pixels), 16):
|
||||
chunk = pixels[i:i + 16]
|
||||
line = ", ".join(f"0x{p:08X}" for p in chunk)
|
||||
f.write(f" {line},\n")
|
||||
|
||||
f.write("};\n")
|
||||
|
||||
print(f"Atlas: {atlas_w}x{atlas_h}, char: {char_w}x{char_h}")
|
||||
print(f"Generated: {png_path}, {h_path}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) >= 2:
|
||||
out_dir = sys.argv[1]
|
||||
else:
|
||||
out_dir = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"assets", "font"
|
||||
)
|
||||
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
generate(out_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
"""
|
||||
将 PNG 图片转换为 C 头文件(uint32_t 像素数组)。
|
||||
|
||||
用法:
|
||||
python tools/png_to_header.py input.png output.h [var_name]
|
||||
|
||||
像素格式: (R << 24) | (G << 16) | (B << 8) | A
|
||||
与 FrameBuffer 的 uint32_t 格式一致。
|
||||
|
||||
如果图片没有 alpha 通道,A 默认为 0xFF。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def convert(input_path: str, output_path: str, var_name: str):
|
||||
img = Image.open(input_path).convert("RGBA")
|
||||
w, h = img.size
|
||||
pixels = list(img.getdata()) if not hasattr(img, "get_flattened_data") else list(img.get_flattened_data())
|
||||
|
||||
packed = []
|
||||
for r, g, b, a in pixels:
|
||||
packed.append((r << 24) | (g << 16) | (b << 8) | a)
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write("// Auto-generated by tools/png_to_header.py\n")
|
||||
f.write(f"#pragma once\n#include <cstdint>\n\n")
|
||||
f.write(f"static const int32_t {var_name}_width = {w};\n")
|
||||
f.write(f"static const int32_t {var_name}_height = {h};\n\n")
|
||||
f.write(f"static const uint32_t {var_name}_pixels[] = {{\n")
|
||||
|
||||
for i in range(0, len(packed), 16):
|
||||
chunk = packed[i : i + 16]
|
||||
line = ", ".join(f"0x{p:08X}" for p in chunk)
|
||||
f.write(f" {line},\n")
|
||||
|
||||
f.write("};\n")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(f"用法: {sys.argv[0]} input.png output.h [var_name]")
|
||||
sys.exit(1)
|
||||
|
||||
input_path = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
|
||||
if len(sys.argv) >= 4:
|
||||
var_name = sys.argv[3]
|
||||
else:
|
||||
# 从文件名生成变量名: player_idle.png -> player_idle
|
||||
var_name = os.path.splitext(os.path.basename(input_path))[0]
|
||||
|
||||
convert(input_path, output_path, var_name)
|
||||
print(f"已生成: {output_path} (var: {var_name}, 像素数: {var_name}_width * {var_name}_height)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue