都会明武电子 LOGO 都会明武电子电子模块 · 技术支持

ESP32 2.8黄屏 · 3D旋转演示 + SD卡视频播放

整数定点3D旋转渲染,参考安富莱STM32H7例程(本质是Pawel A. Hernik的开源算法)移植; 点一下屏幕还能切到SD卡JPEG逐帧视频播放。

ESP32-2432S028R (CYD) TFT_eSPI网页烧录已实测 定点数学3D渲染 SPI 80MHz SD卡+TJpg_Decoder 触摸切换

这次做的内容

4种3D模型

单立方体 / 十字形复合体 / 花瓣状复合体 / 3x3立方体阵列,每6秒自动切换。

5种背景

3种像素图案 + 1种星空穿梭 + 1种棋盘格,跟模型一起自动循环。

SPI提速到硬件上限

40MHz起步~28fps,实测提到80MHz后~52fps;测过100MHz发现帧率跟80MHz完全一样,验证了80MHz正好是ESP32 APB总线本身的硬件天花板。

点屏幕切视频播放

同一份固件加了SD卡JPEG逐帧播放模式,点一下屏幕在3D演示和视频播放之间切换。

算法来源

核心渲染算法(定点sin/cos查表、无浮点、逐行光栅化、背面剔除、Z排序)来自 Pawel A. Hernik 2019年的开源项目 "3D Filled Vector Graphics"(原本面向Arduino+ST7735小屏), YouTube演示。 参考资料里安富莱STM32H7 800x480裸机版本把这个算法原样保留,只是换了显示输出; 这次移植到本板(320x240)时同样原样保留了顶点/面/配色数据和光栅化算法,只换了 屏幕尺寸和显示驱动(STM32的LTDC+DMA2D换成TFT_eSPI的pushImage)。

接线说明

屏幕(ST7789,跟同目录03/04工程一致):

信号GPIO
MOSIIO13
SCLKIO14
CSIO15
DCIO2
BL(背光)IO21

SD卡(走ESP32默认VSPI硬件引脚,跟屏幕是两条独立总线不冲突):

信号GPIO
CSIO5
SCKIO18
MISOIO19
MOSIIO23

SD卡引脚依据是厂商原理图(ESP32-2432S028-MCU.jpg 的SD_Car部分)和厂商自带demo (6_1_SD_Jpg)两处互相印证,不是网上通用资料猜的。

触摸中断脚: IO36,只用来判断"有没有点一下屏幕"(IRQ被拉低), 不解析XPT2046坐标——点屏幕任意位置都能触发3D/视频模式切换。

性能实测

SPI时钟单帧耗时帧率备注
40MHz~35ms~28fps沿用同目录其它工程的默认值
80MHz~19ms~52fps实机长时间点亮确认不花屏
100MHz~19ms~52fps跟80MHz完全一样,证实80MHz是硬件上限(等于ESP32 APB总线时钟本身)

瓶颈是SPI往屏幕搬像素数据的带宽,不是ESP32算3D的算力——这份demo每帧要把 320x240整屏像素全部重画(背景铺满+3D图形),跟B站上常见的"STM32F103C8T6+ST7735 实现80帧"对比不公平:那块屏只有128x160(约2万像素),这块屏是320x240(约7.7万像素), 要搬的数据量差了将近4倍。

SD卡视频播放怎么做的

ESP32算力和内存都不够直接解码标准视频编码(H.264等),做法是先把视频转成 一串JPEG图片(逐帧),存到SD卡,固件按顺序读一张画一张,靠固定的帧间隔造出 "播放视频"的效果,本质是快速幻灯片,不是真正的视频解码器。

视频源

Blender基金会官方开放电影《Big Buck Bunny》(CC BY 3.0),剪了40秒高潮片段。

帧序列

320x160,320张JPEG,共3.9MB,存在SD卡 /bbb/ 目录下(原始素材是180高,sprite内存踩坑后缩到160,见下面"踩坑记录")。

解码

TJpg_Decoder 解码进一块320x160的离屏sprite,解完整块一次性推上屏,不会露出解码到一半的中间状态。

切换

点一下屏幕(判断IO36触摸中断脚),在3D演示和视频播放两个模式之间切换。

署名要求:Big Buck Bunny 是 CC BY 3.0 协议,免费能用,但公开发布时按协议要求 署名 "Blender Foundation | bigbuckbunny.org"。

踩坑记录

问题原因解决
串口上传帧序列,每个文件都缺字节ESP32默认串口接收缓冲只有256字节,SD写入阻塞时被写爆丢数据接收端Serial.setRxBufferSize(16384)
切帧时画面闪黑解码块直接画在屏幕上,能看到"旧帧被新帧一块块覆盖"的中间过程先解码进离屏sprite,解完整块一次性pushSprite
加上sprite后视频模式直接黑屏卡死320x180的sprite要115200字节,但堆内存碎片化,最大连续块只有110580字节,createSprite失败sprite高度缩到160(102400字节),牺牲一点底部裁切换空间
播放速度只有~4fps,达不到预期SD默认用保守的4MHz初始化提到25MHz,解码耗时226ms降到~140ms(~6-7fps);测过40MHz没有进一步提升,瓶颈变成JPEG解码本身的算力

效果预览

完整版实拍演示+源码解说(11分19秒),前半段实机效果,后半段逐行代码拆开讲:

下面是桌面仿真渲染器跑出来的干净素材(跟固件用同一套定点数学和同一份数据, 只是没有实拍摄像头的反光/摩尔纹),横版1920x1080,覆盖全部4种模型x5种背景组合, 配了一段自己合成的原创8-bit背景音乐(纯波形生成,没有版权问题):

跟实拍对比一下——下面这张是SD卡视频播放模式的实机照片,同样是Big Buck Bunny的画面:

实拍照片:SD卡视频播放效果

完整源码(main.ino)

下面是这个项目当前的完整源码,跟仓库里 02_Arduino源码/ESP32_3D_Rotation/src/main.ino 保持同步,可以直接在这页上对着看, 不用另外下载工程。前段的顶点/花纹数据是数值表,篇幅比较长,直接往下滚就能跳过。


/*
  ESP32-2432S028R (CYD, 2.8寸黄屏) 整数定点 3D 旋转演示

  算法来源:Pawel A. Hernik, "3D Filled Vector Graphics" (2019)
  YouTube: https://youtu.be/5y28ipwQs-E
  原版面向 Arduino + ST7735,无浮点、无三角函数库,靠查表 sin/cos + 扫描线
  光栅化实现平面填色的旋转多面体。

  本文件移植自参考资料里安富莱 STM32H7 800x480 裸机版本
  (../01_参考资料/基于H7的3D旋转裸机移植(800x480).7z, User/3DTest.c),
  顶点/面/配色数据和光栅化算法原样保留,改动的地方:
    - 屏幕尺寸从 800x480 换成本板 320x240 (landscape)
    - 显示驱动从 STM32 LTDC+DMA2D 换成 TFT_eSPI 的 pushImage 分行刷屏
    - setup()/loop() 换成 Arduino 框架写法,用 millis() 做切换计时
    - 原来 STM32 例程里没用到的摇杆/按键/串口调试代码删掉了

  硬件:ST7789 320x240 (setRotation(1)),接线沿用同目录下其它工程的
  标准引脚(见 02_所需库与配置/platformio.ini)。
*/

#include <TFT_eSPI.h>
#include <SD.h>
#include <SPI.h>
#include <TJpg_Decoder.h>

TFT_eSPI tft = TFT_eSPI();

// SD卡(TF卡)接线:这块板子SD走的是ESP32默认VSPI硬件引脚(SCK18/MISO19/MOSI23),
// 跟TFT的自定义HSPI引脚(13/14/15)是两条独立总线,不冲突,SD.begin(SD_CS)不用另外配置SPI。
// 引脚来自厂商原理图 ESP32-2432S028-MCU.jpg 的 SD_Car 部分,跟厂商demo(6_1_SD_Jpg)一致。
#define SD_CS 5

// 触摸中断脚:平时高电平,手指按下会被拉低。这里只用它做"有没有点一下屏幕"的
// 开关判断,不读XPT2046坐标(不需要精确点哪里,只要能切模式)。
#define TOUCH_IRQ 36

// mode: 0=3D旋转演示,1=SD卡视频播放,点一下屏幕切换
int mode = 0;
bool touchWasDown = false;
unsigned long lastToggleMs = 0;

#define VIDEO_FRAME_COUNT 320
#define VIDEO_FPS 8
#define VIDEO_FRAME_H 180
int videoFrameIdx = 1;
unsigned long lastVideoFrameMs = 0;

#define SCR_WD 320
#define SCR_HT 240
#define WD_3D  320
#define HT_3D  240

#define SWAP_INT(a, b) { int t_ = a; a = b; b = t_; }

#define NLINES 40
uint16_t frBuf[SCR_WD*NLINES];
int yFr = 0;

int bgMode = 3;
int object = 1;
int bfCull = 1;

// ------------------------------------------------
#define MAXSIN 255
const uint8_t sinTab[91] = {
0,4,8,13,17,22,26,31,35,39,44,48,53,57,61,65,70,74,78,83,87,91,95,99,103,107,111,115,119,123,
127,131,135,138,142,146,149,153,156,160,163,167,170,173,177,180,183,186,189,192,195,198,200,203,206,208,211,213,216,218,
220,223,225,227,229,231,232,234,236,238,239,241,242,243,245,246,247,248,249,250,251,251,252,253,253,254,254,254,254,254,
255
};

int fastSin(int i)
{
  while(i<0) i+=360;
  while(i>=360) i-=360;
  if(i<90)  return((sinTab[i])); else
  if(i<180) return((sinTab[180-i])); else
  if(i<270) return(-(sinTab[i-180])); else
            return(-(sinTab[360-i]));
}

int fastCos(int i)
{
  return fastSin(i+90);
}

// ------------------------------------------------
// RGB565,直接对应 TFT_eSPI 的颜色格式(setSwapBytes(true) 已在 setup() 里开启)
#define RGB(r,g,b) (uint16_t)((((r)&0xF8)<<8) | (((g)&0xFC)<<3) | ((b)>>3))
#define CL_BLACK 0x0000

#define COL11 RGB(0,250,250)
#define COL12 RGB(0,180,180)
#define COL13 RGB(0,210,210)

#define COL21 RGB(250,0,250)
#define COL22 RGB(180,0,180)
#define COL23 RGB(210,0,210)

#define COL31 RGB(250,250,0)
#define COL32 RGB(180,180,0)
#define COL33 RGB(210,210,0)

#define COL41 RGB(250,150,0)
#define COL42 RGB(180,100,0)
#define COL43 RGB(210,140,0)

#define COL51 RGB(0,250,0)
#define COL52 RGB(0,180,0)
#define COL53 RGB(0,210,0)

// ------------------------------------------------
/*
 *        ^Y
 *        |     / Z
 *        |    /
 *    7------6
 *   /|     /|
 *  3------2 |
 *  | |    | |    ----> X
 *  | 4----|-5
 *  |/     |/
 *  0------1
 *
 * object 0: 单个立方体
 * object 1: 十字形(跟 armfly 例程里 object==1 保持一致,也是默认开机形状)
 * object 2: 五角/花瓣状复合立方体
 * object 3: 3x3x1 立方体阵列,不做背面剔除
 */
const int16_t numVerts1 = 8;
const int16_t verts1[] = {
  -100, -100, -100,  // 0
   100, -100, -100,  // 1
   100,  100, -100,  // 2
  -100,  100, -100,  // 3
  -100, -100,  100,  // 4
   100, -100,  100,  // 5
   100,  100,  100,  // 6
  -100,  100,  100,  // 7
};

const int16_t numQuads1 = 6;
const int16_t quads1[] = {
  0, 1, 2, 3,  // front
  7, 6, 5, 4,  // back
  4, 5, 1, 0,  // bottom
  3, 2, 6, 7,  // top
  7, 4, 0, 3,  // left
  2, 1, 5, 6,  // right
};

const uint16_t quadColor1[] = {
  COL33, COL33,COL31,COL31,COL32,COL32,
  //RED, GREEN, BLUE, YELLOW, MAGENTA, CYAN
};
const int16_t numVerts2 = 12*2;
const int16_t verts2[] = {
  -50, -50, -50,  // 0
   50, -50, -50,  // 1
   50,  50, -50,  // 2
  -50,  50, -50,  // 3
  -150,   50, -50,  // 4
  -150,  -50, -50,  // 5
   150,   50, -50,  // 6
   150,  -50, -50,  // 7
   -50,  150, -50,  // 8
    50,  150, -50,  // 9
   -50, -150, -50,  // 10
    50, -150, -50,  // 11

  -50, -50, 50,  // 0
   50, -50, 50,  // 1
   50,  50, 50,  // 2
  -50,  50, 50,  // 3
  -150,   50, 50,  // 4
  -150,  -50, 50,  // 5
   150,   50, 50,  // 6
   150,  -50, 50,  // 7
   -50,  150, 50,  // 8
    50,  150, 50,  // 9
   -50, -150, 50,  // 10
    50, -150, 50,  // 11

};

const int16_t numQuads2 = 5+5+3*4;
const int16_t quads2[] = {
  0, 1, 2, 3,  // front mid
  7, 6, 2, 1,  // front right
  4, 5, 0, 3,  // front left
  3, 2, 9, 8,  // front top
  10,11,1, 0,  // front bottom

  12+3, 12+2, 12+1, 12+0,  // rear mid
  12+1, 12+2, 12+6, 12+7,  // rear right
  12+3, 12+0, 12+5, 12+4,  // rear left
  12+8, 12+9, 12+2, 12+3,  // rear top
  12+0, 12+1,12+11,12+10,  // rear bottom

  0+12,10+12,10,0,
  10+12,11+12,11,10,
  11+12,1+12,1,11,

  1+12,7+12,7,1,
  7+12,6+12,6,7,
  6+12,2+12,2,6,

  2+12,9+12,9,2,
  9+12,8+12,8,9,
  8+12,3+12,3,8,

  3+12,4+12,4,3,
  4+12,5+12,5,4,
  5+12,0+12,0,5,

};

const uint16_t quadColor2[] = {
  COL31,COL51,COL51,COL51,COL51,
  COL31,COL51,COL51,COL51,COL51,
  COL52,COL53,COL52,COL53,COL52,COL53,
  COL52,COL53,COL52,COL53,COL52,COL53,
};
const int16_t numVerts3 = 8*4;
const int16_t verts3[] = {
  -100, -100, -200,  // 0 front
   100, -100, -200,  // 1
   100,  100, -200,  // 2
  -100,  100, -200,  // 3
   -50,  -50,  -50,  // 4
    50,  -50,  -50,  // 5
    50,   50,  -50,  // 6
   -50,   50,  -50,  // 7

   -50,  -50,   50,  // 0 rear
    50,  -50,   50,  // 1
    50,   50,   50,  // 2
   -50,   50,   50,  // 3
  -100, -100,  200,  // 4
   100, -100,  200,  // 5
   100,  100,  200,  // 6
  -100,  100,  200,  // 7

  -200, -100, -100,  // 0 left
   -50,  -50, -50,   // 1
   -50,   50, -50,   // 2
  -200,  100, -100,  // 3
  -200, -100,  100,  // 4
   -50,  -50,  50,   // 5
   -50,   50,  50,   // 6
  -200,  100,  100,  // 7

    50,  -50,  -50,  // 0 right
   200, -100, -100,  // 1
   200,  100, -100,  // 2
    50,   50,  -50,  // 3
    50,  -50,   50,  // 4
   200, -100,  100,  // 5
   200,  100,  100,  // 6
    50,   50,   50,  // 7
};

const int16_t numQuads3 = 6*4;
const int16_t quads3[] = {
  0, 1, 2, 3,  // front
  7, 6, 5, 4,  // back
  4, 5, 1, 0,  // bottom
  3, 2, 6, 7,  // top
  7, 4, 0, 3,  // left
  2, 1, 5, 6,  // right

  8+0, 8+1, 8+2, 8+3,  // front
  8+7, 8+6, 8+5, 8+4,  // back
  8+4, 8+5, 8+1, 8+0,  // bottom
  8+3, 8+2, 8+6, 8+7,  // top
  8+7, 8+4, 8+0, 8+3,  // left
  8+2, 8+1, 8+5, 8+6,  // right

  16+0, 16+1, 16+2, 16+3,  // front
  16+7, 16+6, 16+5, 16+4,  // back
  16+4, 16+5, 16+1, 16+0,  // bottom
  16+3, 16+2, 16+6, 16+7,  // top
  16+7, 16+4, 16+0, 16+3,  // left
  16+2, 16+1, 16+5, 16+6,  // right

  24+0, 24+1, 24+2, 24+3,  // front
  24+7, 24+6, 24+5, 24+4,  // back
  24+4, 24+5, 24+1, 24+0,  // bottom
  24+3, 24+2, 24+6, 24+7,  // top
  24+7, 24+4, 24+0, 24+3,  // left
  24+2, 24+1, 24+5, 24+6,  // right
};

const uint16_t quadColor3[] = {
  COL13,COL13,COL11,COL11,COL12,COL12,
  COL23,COL23,COL21,COL21,COL22,COL22,
  COL33,COL33,COL31,COL31,COL32,COL32,
  COL43,COL43,COL41,COL41,COL42,COL42,
};
const int16_t numVerts4 = 8*9;
const int16_t verts4[] = {

  -50, -50, -50,  // 0
   50, -50, -50,  // 1
   50,  50, -50,  // 2
  -50,  50, -50,  // 3
  -50, -50,  50,  // 4
   50, -50,  50,  // 5
   50,  50,  50,  // 6
  -50,  50,  50,  // 7

  100-50, 100-50, -50+100,  // 0
  100+50, 100-50, -50+100,  // 1
  100+50, 100+50, -50+100,  // 2
  100-50, 100+50, -50+100,  // 3
  100-50, 100-50,  50+100,  // 4
  100+50, 100-50,  50+100,  // 5
  100+50, 100+50,  50+100,  // 6
  100-50, 100+50,  50+100,  // 7

  -100-50, 100-50, -50+100,  // 0
  -100+50, 100-50, -50+100,  // 1
  -100+50, 100+50, -50+100,  // 2
  -100-50, 100+50, -50+100,  // 3
  -100-50, 100-50,  50+100,  // 4
  -100+50, 100-50,  50+100,  // 5
  -100+50, 100+50,  50+100,  // 6
  -100-50, 100+50,  50+100,  // 7

  -100-50, -100-50, -50+100,  // 0
  -100+50, -100-50, -50+100,  // 1
  -100+50, -100+50, -50+100,  // 2
  -100-50, -100+50, -50+100,  // 3
  -100-50, -100-50,  50+100,  // 4
  -100+50, -100-50,  50+100,  // 5
  -100+50, -100+50,  50+100,  // 6
  -100-50, -100+50,  50+100,  // 7

  100-50, -100-50, -50+100,  // 0
  100+50, -100-50, -50+100,  // 1
  100+50, -100+50, -50+100,  // 2
  100-50, -100+50, -50+100,  // 3
  100-50, -100-50,  50+100,  // 4
  100+50, -100-50,  50+100,  // 5
  100+50, -100+50,  50+100,  // 6
  100-50, -100+50,  50+100,  // 7

  100-50, 100-50, -50-100,  // 0
  100+50, 100-50, -50-100,  // 1
  100+50, 100+50, -50-100,  // 2
  100-50, 100+50, -50-100,  // 3
  100-50, 100-50,  50-100,  // 4
  100+50, 100-50,  50-100,  // 5
  100+50, 100+50,  50-100,  // 6
  100-50, 100+50,  50-100,  // 7

  -100-50, 100-50, -50-100,  // 0
  -100+50, 100-50, -50-100,  // 1
  -100+50, 100+50, -50-100,  // 2
  -100-50, 100+50, -50-100,  // 3
  -100-50, 100-50,  50-100,  // 4
  -100+50, 100-50,  50-100,  // 5
  -100+50, 100+50,  50-100,  // 6
  -100-50, 100+50,  50-100,  // 7

  -100-50, -100-50, -50-100,  // 0
  -100+50, -100-50, -50-100,  // 1
  -100+50, -100+50, -50-100,  // 2
  -100-50, -100+50, -50-100,  // 3
  -100-50, -100-50,  50-100,  // 4
  -100+50, -100-50,  50-100,  // 5
  -100+50, -100+50,  50-100,  // 6
  -100-50, -100+50,  50-100,  // 7

  100-50, -100-50, -50-100,  // 0
  100+50, -100-50, -50-100,  // 1
  100+50, -100+50, -50-100,  // 2
  100-50, -100+50, -50-100,  // 3
  100-50, -100-50,  50-100,  // 4
  100+50, -100-50,  50-100,  // 5
  100+50, -100+50,  50-100,  // 6
  100-50, -100+50,  50-100,  // 7
};

const int16_t numQuads4 = 6*9;
const int16_t quads4[] = {
  0, 1, 2, 3,  // front
  7, 6, 5, 4,  // back
  4, 5, 1, 0,  // bottom
  3, 2, 6, 7,  // top
  7, 4, 0, 3,  // left
  2, 1, 5, 6,  // right

  8+0, 8+1, 8+2, 8+3,  // front
  8+7, 8+6, 8+5, 8+4,  // back
  8+4, 8+5, 8+1, 8+0,  // bottom
  8+3, 8+2, 8+6, 8+7,  // top
  8+7, 8+4, 8+0, 8+3,  // left
  8+2, 8+1, 8+5, 8+6,  // right

  16+0, 16+1, 16+2, 16+3,  // front
  16+7, 16+6, 16+5, 16+4,  // back
  16+4, 16+5, 16+1, 16+0,  // bottom
  16+3, 16+2, 16+6, 16+7,  // top
  16+7, 16+4, 16+0, 16+3,  // left
  16+2, 16+1, 16+5, 16+6,  // right

  24+0, 24+1, 24+2, 24+3,  // front
  24+7, 24+6, 24+5, 24+4,  // back
  24+4, 24+5, 24+1, 24+0,  // bottom
  24+3, 24+2, 24+6, 24+7,  // top
  24+7, 24+4, 24+0, 24+3,  // left
  24+2, 24+1, 24+5, 24+6,  // right

  32+0, 32+1, 32+2, 32+3,  // front
  32+7, 32+6, 32+5, 32+4,  // back
  32+4, 32+5, 32+1, 32+0,  // bottom
  32+3, 32+2, 32+6, 32+7,  // top
  32+7, 32+4, 32+0, 32+3,  // left
  32+2, 32+1, 32+5, 32+6,  // right

  40+0, 40+1, 40+2, 40+3,  // front
  40+7, 40+6, 40+5, 40+4,  // back
  40+4, 40+5, 40+1, 40+0,  // bottom
  40+3, 40+2, 40+6, 40+7,  // top
  40+7, 40+4, 40+0, 40+3,  // left
  40+2, 40+1, 40+5, 40+6,  // right

  48+0, 48+1, 48+2, 48+3,  // front
  48+7, 48+6, 48+5, 48+4,  // back
  48+4, 48+5, 48+1, 48+0,  // bottom
  48+3, 48+2, 48+6, 48+7,  // top
  48+7, 48+4, 48+0, 48+3,  // left
  48+2, 48+1, 48+5, 48+6,  // right

  56+0, 56+1, 56+2, 56+3,  // front
  56+7, 56+6, 56+5, 56+4,  // back
  56+4, 56+5, 56+1, 56+0,  // bottom
  56+3, 56+2, 56+6, 56+7,  // top
  56+7, 56+4, 56+0, 56+3,  // left
  56+2, 56+1, 56+5, 56+6,  // right

  64+0, 64+1, 64+2, 64+3,  // front
  64+7, 64+6, 64+5, 64+4,  // back
  64+4, 64+5, 64+1, 64+0,  // bottom
  64+3, 64+2, 64+6, 64+7,  // top
  64+7, 64+4, 64+0, 64+3,  // left
  64+2, 64+1, 64+5, 64+6,  // right
};

const uint16_t quadColor4[] = {
  COL13,COL13,COL11,COL11,COL12,COL12,
  COL23,COL23,COL21,COL21,COL22,COL22,
  COL33,COL33,COL31,COL31,COL32,COL32,
  COL43,COL43,COL41,COL41,COL42,COL42,
  COL53,COL53,COL51,COL51,COL52,COL52,
  COL33,COL33,COL31,COL31,COL32,COL32,
  COL43,COL43,COL41,COL41,COL42,COL42,
  COL53,COL53,COL51,COL51,COL52,COL52,
  COL23,COL23,COL21,COL21,COL22,COL22,
};
const unsigned short pat8[0x0400+3] = {
  32,32,16,      // width,height,bits
  0x1840,0x38c0,0x4100,0x4940,0x4940,0x5161,0x4961,0x2060,0x1860,0x4960,0x4940,0x4940,0x4940,0x4920,0x4940,0x4941,
  0x2060,0x4100,0x4920,0x4940,0x4940,0x4940,0x4120,0x1840,0x4960,0x59a1,0x5161,0x4940,0x4940,0x4100,0x4120,0x38e0,
  0x5181,0x1020,0x38e0,0x4960,0x5181,0x4961,0x2060,0x4120,0x4120,0x1860,0x5160,0x4940,0x4940,0x4940,0x4920,0x4940,
  0x5161,0x2060,0x4100,0x4940,0x4940,0x4940,0x2080,0x4940,0x2080,0x5181,0x59a1,0x5181,0x4960,0x4940,0x4100,0x38e0,
  0x5160,0x4961,0x1840,0x4120,0x4961,0x2060,0x4140,0x4961,0x4940,0x30c0,0x1840,0x4940,0x5160,0x4940,0x4920,0x4940,
  0x4940,0x4961,0x2080,0x4120,0x4120,0x2060,0x5181,0x5161,0x4120,0x2060,0x5181,0x59a1,0x5161,0x4960,0x4940,0x4100,
  0x4920,0x5181,0x4941,0x1840,0x2060,0x4100,0x4941,0x4120,0x38e0,0x4100,0x4100,0x1840,0x4940,0x5181,0x4940,0x4940,
  0x4920,0x4941,0x5181,0x1840,0x1840,0x4940,0x5161,0x3900,0x4120,0x4961,0x2080,0x4961,0x5181,0x4941,0x5181,0x5181,
  0x59c2,0x4920,0x4120,0x2060,0x3900,0x4140,0x4941,0x30c0,0x3900,0x4120,0x4920,0x1840,0x1840,0x5160,0x5181,0x4940,
  0x4940,0x4920,0x4121,0x1840,0x38e0,0x4941,0x4100,0x4120,0x5161,0x5181,0x5161,0x1840,0x4940,0x4940,0x4941,0x59a1,
  0x59a1,0x4961,0x1840,0x4120,0x4981,0x4981,0x28a0,0x30c0,0x4140,0x4140,0x1840,0x30c0,0x38e0,0x1840,0x4960,0x5181,
  0x4120,0x4100,0x1840,0x4120,0x4940,0x4941,0x5161,0x5161,0x5181,0x5181,0x2080,0x4940,0x1860,0x4120,0x4920,0x5161,
  0x38c0,0x2880,0x4920,0x4940,0x51a1,0x28a0,0x30c0,0x4100,0x4120,0x1860,0x4121,0x4121,0x38e0,0x4100,0x1860,0x4120,
  0x4120,0x1840,0x4100,0x4120,0x5161,0x5181,0x5181,0x5160,0x51a1,0x2060,0x4100,0x5181,0x4940,0x1860,0x4100,0x4100,
  0x1020,0x4120,0x4940,0x5181,0x3900,0x38e0,0x4120,0x30c0,0x1840,0x4120,0x4121,0x4120,0x3900,0x3901,0x4141,0x1840,
  0x1840,0x4120,0x4920,0x4941,0x5181,0x5181,0x5160,0x4140,0x2060,0x4140,0x4940,0x4120,0x4961,0x4920,0x2060,0x30a0,
  0x1840,0x4120,0x4940,0x4120,0x4100,0x4140,0x4100,0x1840,0x1840,0x4120,0x3900,0x3900,0x38e0,0x3900,0x4981,0x4120,
  0x1840,0x4100,0x5161,0x5181,0x5181,0x5180,0x5161,0x1860,0x1840,0x4940,0x4940,0x4120,0x4120,0x4940,0x4920,0x1860,
  0x28a0,0x4100,0x4940,0x4120,0x4920,0x3900,0x2060,0x5161,0x4120,0x1840,0x3900,0x38e0,0x30e0,0x38e0,0x4120,0x4961,
  0x4100,0x1840,0x4121,0x5161,0x5180,0x4960,0x2060,0x4100,0x4120,0x2060,0x4100,0x4920,0x3900,0x38e0,0x4940,0x4120,
  0x5181,0x2060,0x38e0,0x4120,0x4120,0x1860,0x59a1,0x5161,0x4920,0x3900,0x1840,0x4120,0x3920,0x38e0,0x4100,0x4140,
  0x4100,0x38c0,0x1840,0x4940,0x5161,0x2060,0x4100,0x4940,0x5160,0x4940,0x2060,0x4120,0x4120,0x30c0,0x38e0,0x4940,
  0x4920,0x5181,0x1860,0x3900,0x2060,0x51a1,0x5181,0x4100,0x30c0,0x38e0,0x4120,0x1840,0x4121,0x4120,0x4120,0x4120,
  0x4121,0x38e0,0x38e0,0x1840,0x1840,0x4940,0x4920,0x5160,0x59e1,0x5181,0x4120,0x1860,0x3900,0x4940,0x38e0,0x38e0,
  0x4940,0x4920,0x4941,0x2060,0x4961,0x5161,0x4100,0x30c0,0x38e0,0x4940,0x4940,0x1840,0x1840,0x4120,0x4940,0x4120,
  0x4120,0x4941,0x38c0,0x1840,0x38e0,0x4940,0x5160,0x59c1,0x5181,0x4960,0x4100,0x2060,0x1860,0x4941,0x4940,0x40e0,
  0x5161,0x4100,0x2060,0x59a1,0x4960,0x40e0,0x30c0,0x4120,0x4961,0x4940,0x1840,0x4100,0x4120,0x1840,0x4120,0x4940,
  0x4120,0x4120,0x1840,0x4940,0x5160,0x5180,0x59c1,0x5181,0x4960,0x4100,0x1840,0x3900,0x38e0,0x1860,0x5161,0x4940,
  0x38e0,0x2080,0x51a1,0x5161,0x40e0,0x3900,0x4120,0x5161,0x4961,0x1840,0x4940,0x4940,0x5181,0x4940,0x1860,0x4120,
  0x4120,0x1840,0x4920,0x5181,0x4940,0x59c1,0x5181,0x5181,0x4120,0x1840,0x4120,0x38e0,0x4100,0x3900,0x2060,0x5181,
  0x1840,0x5181,0x5181,0x4100,0x4100,0x4940,0x4960,0x3900,0x1840,0x4940,0x5161,0x4920,0x4960,0x5160,0x4940,0x2060,
  0x1860,0x4940,0x5160,0x4960,0x4920,0x5181,0x5181,0x38e0,0x1860,0x4120,0x4940,0x4120,0x4120,0x5161,0x4960,0x1840,
  0x1860,0x4940,0x4920,0x4920,0x4940,0x4940,0x4941,0x1840,0x1820,0x4100,0x5160,0x4940,0x4920,0x5160,0x5160,0x4941,
  0x2060,0x4120,0x5160,0x4920,0x5160,0x5160,0x4920,0x1840,0x1840,0x4941,0x4940,0x4940,0x5160,0x5181,0x59a1,0x4140,
  0x4941,0x1840,0x4120,0x4960,0x5161,0x4941,0x1840,0x4120,0x3900,0x1840,0x4100,0x38e0,0x38e0,0x4940,0x5160,0x4140,
  0x4121,0x2060,0x4120,0x4920,0x5160,0x4100,0x1840,0x4120,0x4120,0x1840,0x4941,0x5161,0x5181,0x61e1,0x5181,0x59a1,
  0x4100,0x4941,0x1840,0x4120,0x4940,0x1840,0x4960,0x51a1,0x4961,0x4120,0x1020,0x4120,0x30e0,0x38e0,0x4960,0x4940,
  0x38e0,0x4941,0x2060,0x4120,0x4920,0x1840,0x3900,0x4120,0x4941,0x4121,0x1840,0x4940,0x4941,0x5181,0x6222,0x5161,
  0x4100,0x4100,0x4941,0x1840,0x1040,0x4960,0x51a1,0x4120,0x4120,0x38e0,0x3900,0x1840,0x4100,0x38e0,0x4100,0x4940,
  0x4100,0x38e0,0x4120,0x1840,0x1820,0x3900,0x4100,0x4941,0x4121,0x4120,0x3900,0x1840,0x4100,0x4941,0x59c1,0x59c1,
  0x59a1,0x4100,0x3900,0x1840,0x4100,0x59c1,0x4100,0x3900,0x30c0,0x4100,0x4940,0x1840,0x1840,0x4940,0x38e0,0x4920,
  0x4940,0x4120,0x4120,0x1840,0x30c0,0x4120,0x4961,0x4141,0x4120,0x4120,0x3900,0x1840,0x1840,0x5161,0x4921,0x5181,
  0x61e2,0x4141,0x1840,0x4140,0x59c1,0x4120,0x30e0,0x30c0,0x38e0,0x4120,0x2060,0x4120,0x4100,0x2060,0x4120,0x38e0,
  0x4940,0x4100,0x1840,0x38e0,0x3900,0x4961,0x4121,0x38e0,0x3900,0x38e0,0x1860,0x4941,0x38e0,0x1860,0x5161,0x4921,
  0x4941,0x2880,0x38e0,0x4941,0x4121,0x30c0,0x28a0,0x30c0,0x4120,0x1860,0x5181,0x5181,0x4940,0x4940,0x2060,0x4121,
  0x3900,0x1840,0x3900,0x38e0,0x4160,0x3900,0x30c0,0x38e0,0x38e0,0x1040,0x4940,0x59c1,0x5181,0x38e0,0x2060,0x5161,
  0x1020,0x4100,0x4941,0x4121,0x30c0,0x2080,0x30e0,0x30e0,0x1840,0x4941,0x4940,0x5161,0x5181,0x4960,0x4940,0x2060,
  0x2060,0x4120,0x3900,0x3900,0x30e0,0x28a0,0x30c0,0x30c0,0x1040,0x4120,0x4940,0x61e1,0x59c1,0x5181,0x4120,0x1860,
  0x1020,0x4100,0x4941,0x38e0,0x30c0,0x4100,0x4940,0x1860,0x1840,0x4961,0x5181,0x51a1,0x5181,0x5181,0x4940,0x4120,
  0x2080,0x3900,0x4120,0x3900,0x30c0,0x30c0,0x38e0,0x1840,0x1840,0x5161,0x5160,0x4940,0x6201,0x59a1,0x5161,0x4100,
  0x38c0,0x1020,0x3900,0x4120,0x4120,0x4940,0x2060,0x4941,0x4120,0x1840,0x51a1,0x6202,0x5181,0x5181,0x5181,0x4960,
  0x4940,0x2080,0x3920,0x38e0,0x38e0,0x4121,0x1840,0x4141,0x4120,0x2060,0x5161,0x4940,0x51a1,0x61e1,0x5181,0x5161,
  0x4940,0x38e0,0x1020,0x4120,0x4940,0x2060,0x4941,0x5161,0x4920,0x30c0,0x1860,0x59a2,0x61e1,0x5161,0x5181,0x5181,
  0x4940,0x4940,0x2080,0x3900,0x4941,0x1840,0x51a1,0x61e2,0x4961,0x4121,0x2060,0x4940,0x5160,0x59c1,0x5181,0x4961,
  0x51a1,0x4940,0x3900,0x1020,0x2060,0x4940,0x5161,0x4940,0x4100,0x4920,0x4961,0x2060,0x51a1,0x61e2,0x4940,0x5161,
  0x5161,0x4940,0x4120,0x1860,0x1860,0x5181,0x6222,0x51a1,0x4961,0x5161,0x4120,0x1860,0x3900,0x5181,0x59a1,0x5181,
  0x59c1,0x5181,0x3900,0x2060,0x4120,0x4960,0x4920,0x4100,0x4940,0x59a1,0x59c1,0x1840,0x1860,0x59e2,0x59a1,0x4920,
  0x5181,0x5181,0x4120,0x1840,0x4961,0x6201,0x59c1,0x5181,0x5161,0x4940,0x4120,0x1840,0x1840,0x4921,0x59a1,0x61e2,
  0x5181,0x4961,0x2060,0x4940,0x5160,0x4920,0x4100,0x4940,0x59a1,0x51a1,0x2060,0x4120,0x38e0,0x2060,0x59c2,0x4960,
  0x4940,0x4961,0x2080,0x4120,0x59a1,0x59a1,0x5181,0x5161,0x4941,0x38e0,0x1840,0x4120,0x3900,0x1840,0x4920,0x59a1,
  0x38e0,0x2080,0x4940,0x5160,0x4940,0x4100,0x4940,0x59a1,0x51a1,0x2060,0x4940,0x59a1,0x4940,0x3900,0x2060,0x51a1,
  0x4120,0x2060,0x4940,0x4940,0x59a1,0x5181,0x5160,0x4940,0x4100,0x1840,0x38e0,0x4940,0x5160,0x38e0,0x1840,0x4100,
  0x1840,0x4920,0x4940,0x4940,0x4940,0x4940,0x59a1,0x4141,0x1840,0x4120,0x5160,0x5181,0x5160,0x4960,0x3900,0x2060,
  0x2880,0x4940,0x4960,0x5160,0x5181,0x5160,0x4960,0x38e0,0x1860,0x4941,0x4940,0x4940,0x4940,0x4940,0x38c0,0x1020,
};
const unsigned short pat7[0x0400+3] = {
  32,32,16,      // width,height,bits
  0x8b86,0x8b86,0x4180,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0x5a84,0x5a84,0x5a84,0x5a84,0x5a84,
  0x5a84,0x41e1,0x41e1,0x41e1,0x41e1,0x5a21,0x5a21,0x5a21,0x41e1,0x41e1,0x4180,0xa448,0xa448,0xa448,0xa448,0xa448,
  0x8b86,0x8b86,0x4180,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0x7346,0x7346,0x7346,0x7346,0x7346,
  0x5a84,0x41e1,0xa448,0xa448,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x4180,0xa448,0x8b86,0x8b86,0x8b86,0x8b86,
  0x8b86,0x8b86,0x4180,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0x4180,0x7346,0x7346,0x7346,0x7346,0x7346,
  0x5a84,0x41e1,0xa448,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x4180,0xa448,0xa446,0x8b86,0x8b86,0x8b86,
  0x4180,0x4180,0x4180,0x4180,0x5a21,0x5a21,0x4180,0x5a21,0x5a21,0x5a21,0x4180,0x5a84,0x5a84,0x5a84,0x5a84,0x5a84,
  0x5a84,0x4180,0x4180,0x4180,0x5a21,0x7263,0x7263,0x7263,0x7263,0x4180,0x4180,0x4180,0x5a21,0x5a21,0x4180,0x4180,
  0x8b84,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0x7263,0x5a21,0x5a21,0x5a21,0x5a21,0x5a21,0x7263,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x5a21,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,
  0x8b84,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0x7263,0x5a21,0x5a21,0x5a21,0x5a21,0x5a21,0x7263,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0xa446,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,
  0x8b84,0x8b84,0x8b84,0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0x7263,0x5a21,0x5a21,0x5a21,0x5a21,0x5a21,0x7263,
  0x8b84,0x8b84,0x8b84,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0xa446,0xa446,0xa446,0x8b84,0x8b84,0x8b84,0x8b84,
  0x4180,0x4180,0x5a21,0x5a21,0x5a21,0x7263,0x5a21,0x5a21,0x41e1,0x7263,0x7263,0x5a21,0x41e1,0x7263,0x7263,0x7263,
  0x7263,0x41e1,0x41e1,0x41e1,0x7263,0x7263,0x5a21,0x5a21,0x5a21,0x41e1,0x41e1,0x41e1,0x5a21,0x5a21,0x5a21,0x5a21,
  0x7263,0x7263,0x5a21,0xa448,0xa448,0xa448,0x8b86,0x5a21,0x7263,0x7263,0x7263,0x7263,0x41e1,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x41e1,0xa446,0xa446,0x8b84,0x8b84,0x5a21,0x7263,0x7263,0x7263,0x41e1,0x7263,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x5a21,0xa448,0xa446,0x8b86,0x8b86,0x4180,0x7263,0x7263,0x7263,0x7263,0x5a21,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x5a21,0xa446,0xa446,0x8b84,0x8b84,0x4180,0x7263,0x7263,0x7263,0x5a21,0x7263,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x5a21,0xa448,0x8b86,0x8b86,0x8b86,0x4180,0x8b84,0x7263,0x7263,0x7263,0x5a21,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x5a21,0xa446,0xa446,0xa446,0x8b84,0x4180,0x8b84,0x8b84,0x8b84,0x8b84,0x7263,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0x7263,0x7263,0x7263,0x7263,0x7263,0x5a21,0x8b84,0x8b84,0x8b84,0x8b84,
  0x8b84,0x8b84,0x5a21,0x5a21,0x5a21,0x41e1,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0x4180,0x4180,0x4180,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0x7263,0x7263,0x7263,0x7263,0x7263,0x5a21,0x8b84,0x8b84,0x8b84,0x8b84,
  0x8b84,0x8b84,0x5a21,0x5a21,0x5a21,0x41e1,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0x5a21,0x8b84,0x8b84,0x8b84,0x7263,0x5a21,0x8b84,0x8b84,0x8b84,0x8b84,
  0x8b84,0x8b84,0x5a21,0x5a21,0x5a21,0x41e1,0x5a21,0x8b84,0x8b84,0x8b84,0x7263,0x7263,0x7263,0x7263,0x5a21,0x4180,
  0x5a21,0x4180,0x4180,0x4180,0x5a21,0x41e1,0x4180,0x5a21,0x5a21,0x5a21,0x4180,0x4180,0x5a21,0x5a21,0x4180,0x4180,
  0x5a21,0x4180,0x5a21,0x4180,0x5a21,0x41e1,0x4180,0x5a21,0x5a21,0x5a21,0x4180,0x4180,0x5a21,0x5a21,0x4180,0x4180,
  0x8b84,0x7263,0x4180,0x8b86,0xa448,0x8b86,0x8b86,0x8b86,0x8b84,0x4180,0x8b84,0xa446,0x8b84,0x8b84,0x7263,0x7263,
  0x7263,0x7263,0x4180,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0xa446,0x8b84,0x8b84,0x8b84,0x8b84,
  0x8b84,0x7263,0x4180,0x8b86,0x8b86,0x8b86,0x8b84,0x8b84,0x8b84,0x4180,0x8b84,0xa446,0xa446,0x8b84,0x7263,0x7263,
  0x7263,0x7263,0x4180,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0xa446,0xa446,0x8b84,0x8b84,0x8b84,
  0x8b84,0x7263,0x4180,0x8b86,0x8b86,0x8b84,0x8b84,0x8b84,0x8b84,0x4180,0x8b84,0x8b84,0x8b84,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x4180,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0xa446,0xa446,0x8b84,0x8b84,0x8b84,
  0x5a21,0x4180,0x4180,0x4180,0x5a21,0x5a21,0x4180,0x7263,0x7263,0x7263,0x7263,0x7263,0x5a21,0x5a21,0x7263,0x4180,
  0x4180,0x4180,0x4180,0x4180,0x5a21,0x5a21,0x4180,0x4180,0x41e1,0x41e1,0x41e1,0x4180,0x5a21,0x5a21,0x5a21,0x4180,
  0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x5a21,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,
  0x7263,0xa446,0xa446,0xa446,0xa446,0xa446,0x8b84,0x8b84,0x41e1,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x7263,0x4180,
  0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x4180,0xa446,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x7263,
  0x7263,0xbce6,0xa446,0xa446,0xa446,0xa446,0x8b84,0x8b84,0x7263,0xa446,0x8b84,0x8b84,0x8b84,0x8b84,0x7263,0x4180,
  0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x7346,0x4180,0xa446,0xa446,0xa446,0x8b84,0x8b84,0x8b84,0x7263,
  0x7263,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x8b84,0x41e1,0xa446,0xa446,0xa446,0x8b84,0x8b84,0x7263,0x4180,
  0x4180,0x4180,0x4180,0x4180,0x5a21,0x5a21,0x5a21,0x5a21,0x5a21,0x5a21,0x7263,0x41e1,0x41e1,0x41e1,0x7263,0x7263,
  0x5a21,0x5a21,0x41e1,0x5a21,0x5a21,0x5a21,0x41e1,0x41e1,0x41e1,0x41e1,0x7263,0x5a21,0x5a21,0x5a21,0x5a21,0x4180,
  0x7263,0x7263,0x4180,0xa446,0xa446,0x8b84,0x8b84,0x5a21,0x7263,0x7263,0x7263,0x7263,0x41e1,0x8b84,0x8b84,0x8b84,
  0x8b84,0x7263,0x41e1,0xa448,0xa448,0xa448,0x8b86,0x5a21,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x4180,0xa446,0xa446,0x8b84,0x8b84,0x41e1,0x7263,0x7263,0x7263,0x7263,0x41e1,0x8b84,0x8b84,0x8b84,
  0x8b84,0x7263,0x5a21,0xa448,0xa448,0xa448,0x8b86,0x4180,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,
  0x7263,0x7263,0x5a21,0xa446,0xa446,0xa446,0x8b84,0x41e1,0x8b84,0x7263,0x7263,0x7263,0x41e1,0x8b84,0x8b84,0x8b84,
  0x8b84,0x7263,0x5a21,0x8b86,0x8b86,0x8b86,0x8b86,0x4180,0x8b84,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,0x7263,
  0x4180,0x4180,0x4180,0x5a21,0x5a21,0x5a21,0x5a21,0x41e1,0x41e1,0x41e1,0x41e1,0x41e1,0x41e1,0x7263,0x7263,0x41e1,
  0x4180,0x41e1,0x41e1,0x41e1,0x41e1,0x41e1,0x41e1,0x41e1,0x41e1,0x41e1,0x41e1,0x5a21,0x41e1,0x4180,0x4180,0x4180,
  0x5a21,0x5a21,0x5a21,0x5a21,0x5a21,0x41e1,0xa448,0xa448,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x41e1,
  0x7263,0x7263,0x7263,0x4180,0x8b84,0x7346,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x4180,0x5a21,0x5a21,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0xa448,0xa448,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x7346,0x7346,0x41e1,
  0x8b84,0x8b84,0x8b84,0x4180,0x8b84,0x8b84,0x7346,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x4180,0x7263,0x5a21,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0xa448,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x7346,0x5a21,0x4180,
  0x8b84,0x8b84,0x8b84,0x4180,0x8b84,0x8b84,0x8b84,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x4180,0x7263,0x5a21,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x41e1,0xa448,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x7346,0x5a21,0x4180,
  0x8b84,0x8b84,0x8b84,0x4180,0x8b84,0x8b84,0x8b84,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x8b86,0x4180,0x7263,0x5a21,
  0x5a21,0x4180,0x4180,0x4180,0x5a21,0x5a21,0x5a84,0x5a84,0x5a84,0x5a84,0x5a84,0x5a84,0x5a84,0x5a84,0x7263,0x4180,
  0x7263,0x7263,0x7263,0x7263,0x7263,0x3140,0x7263,0x7263,0x7263,0x7263,0x7263,0x4180,0x5a21,0x5a21,0x4180,0x4180,
};
const unsigned short pat2[0x0400+3] = {
  32,32,16,      // width,height,bits
  0x3829,0x1804,0x3809,0x500d,0x500d,0x500d,0x500d,0x480c,0x480c,0x500d,0x500d,0x584e,0x70d1,0x3809,0x1003,0x1803,
  0x3809,0x400b,0x480b,0x500d,0x70f2,0x7913,0x8113,0x70b2,0x402b,0x1804,0x3007,0x500d,0x500d,0x500d,0x500d,0x8173,
  0x508c,0x1804,0x3809,0x500e,0x500d,0x606f,0x608f,0x606e,0x502d,0x480c,0x500d,0x8133,0x68b0,0x2005,0x1003,0x1803,
  0x1804,0x2005,0x2006,0x3008,0x588d,0x7111,0x70d1,0x482b,0x2005,0x3008,0x480c,0x582e,0x502d,0x584d,0x582e,0x70f2,
  0x6930,0x1804,0x3008,0x602f,0x604f,0x500d,0x500d,0x500d,0x500d,0x582e,0x8954,0x70d2,0x2807,0x1804,0x2807,0x586e,
  0x70f1,0x68f0,0x486b,0x2006,0x1804,0x2005,0x2806,0x2005,0x3809,0x500d,0x580e,0x502d,0x584d,0x584d,0x502d,0x604f,
  0x81b3,0x1805,0x2807,0x582e,0x8934,0x8173,0x8174,0x8153,0x78d2,0x8955,0x78f2,0x2806,0x1805,0x3829,0x8153,0x9997,
  0x9156,0x9155,0x99b6,0x8975,0x508d,0x1804,0x0801,0x1804,0x3007,0x400b,0x6870,0x6850,0x480c,0x482c,0x482c,0x606f,
  0x8153,0x1805,0x1805,0x400a,0x602f,0x70b2,0x8935,0x9156,0x8935,0x6890,0x2806,0x2005,0x506d,0xa1f7,0xa218,0x78d3,
  0x500e,0x500d,0x580e,0x70b1,0x9175,0x8133,0x508c,0x1804,0x1804,0x2806,0x3829,0x78f2,0x6050,0x480c,0x480c,0x606f,
  0x586e,0x2005,0x1002,0x1804,0x2006,0x3008,0x3809,0x3809,0x3809,0x2005,0x2005,0x60ae,0xaa78,0x99d7,0x78f2,0x604f,
  0x500d,0x500d,0x500d,0x500d,0x6870,0x7091,0x9155,0x7932,0x3809,0x1804,0x1804,0x4069,0x8974,0x78f2,0x604f,0x8133,
  0x2807,0x1804,0x1805,0x2006,0x2006,0x2806,0x2806,0x1804,0x0802,0x1804,0x608f,0xaa79,0x99f6,0x7913,0x6871,0x500e,
  0x500d,0x500d,0x500d,0x582e,0x7953,0x91f5,0x99f6,0x9196,0x8134,0x484b,0x1804,0x1803,0x486b,0x70d1,0x70d1,0x60ce,
  0x1804,0x2806,0x50ad,0x7131,0x7972,0x7952,0x506c,0x3007,0x1804,0x402a,0x99d7,0x91f6,0x91f6,0x6891,0x600f,0x580e,
  0x500d,0x500d,0x500d,0x500d,0x500d,0x68b0,0x7952,0x8153,0x91b5,0x91b5,0x502c,0x1804,0x2005,0x380a,0x400a,0x2806,
  0x3829,0x502d,0x6870,0x78d2,0x91d6,0xaa78,0x9a17,0x406a,0x1804,0x70d1,0x99d7,0x8154,0x8974,0x6890,0x580e,0x500d,
  0x500d,0x582e,0x582e,0x582e,0x582e,0x582e,0x500d,0x582e,0x6870,0x89b5,0x8113,0x3809,0x1003,0x1003,0x1804,0x2005,
  0x6870,0x580e,0x500d,0x582e,0x70d1,0x99d6,0xaa58,0x48cb,0x2006,0x8955,0x9175,0x7913,0x7912,0x68b0,0x580e,0x500d,
  0x480b,0x482c,0x482c,0x502d,0x500d,0x500d,0x500d,0x500d,0x604f,0x8133,0x8974,0x482c,0x1804,0x1805,0x486b,0x7131,
  0x500d,0x500d,0x500d,0x580e,0x70b1,0x91d6,0xaa58,0x40aa,0x3008,0x99d6,0x8955,0x70b1,0x70d1,0x6090,0x500d,0x580e,
  0x480c,0x480b,0x480c,0x480c,0x480c,0x500d,0x480c,0x480c,0x7111,0x7932,0x91f6,0x604f,0x2006,0x3808,0x7912,0x604f,
  0x500d,0x502d,0x502d,0x582e,0x608f,0x7912,0xa258,0x3849,0x3008,0x99f6,0x8975,0x6891,0x606f,0x500d,0x500d,0x500d,
  0x500c,0x584e,0x584e,0x584d,0x502d,0x500c,0x480c,0x502c,0x68d0,0x68b0,0x91d5,0x586e,0x2006,0x4029,0x608f,0x500d,
  0x500d,0x500c,0x502d,0x586e,0x582e,0x6890,0x8995,0x3829,0x2807,0x8173,0x9195,0x7913,0x6870,0x480c,0x500d,0x500d,
  0x502d,0x504d,0x584d,0x584e,0x482c,0x502d,0x502d,0x502d,0x500c,0x584e,0x7912,0x482c,0x1804,0x50cd,0x606f,0x500d,
  0x500d,0x500d,0x582e,0x502d,0x580e,0x7091,0x8154,0x3008,0x2006,0x7932,0x8133,0x8995,0x6870,0x500c,0x500c,0x502d,
  0x586e,0x586e,0x482c,0x400b,0x400b,0x480b,0x480c,0x500d,0x500d,0x500d,0x584f,0x3809,0x2006,0x60ef,0x580e,0x500d,
  0x500d,0x500d,0x500d,0x500d,0x500d,0x606f,0x584d,0x2005,0x1804,0x70f2,0x70d2,0x68b1,0x6850,0x500d,0x480c,0x480c,
  0x584e,0x586e,0x502d,0x480c,0x480c,0x500d,0x500d,0x500d,0x500d,0x500d,0x500d,0x2806,0x3048,0x7111,0x500d,0x500d,
  0x500d,0x500d,0x500d,0x500c,0x480c,0x400a,0x2807,0x1003,0x1003,0x586e,0x8134,0x70d1,0x6891,0x580f,0x500d,0x500c,
  0x480c,0x480c,0x502d,0x502d,0x502d,0x500d,0x500d,0x500d,0x500d,0x500d,0x400b,0x1804,0x3808,0x68b0,0x500d,0x500d,
  0x400b,0x400a,0x3008,0x2807,0x2806,0x2005,0x1804,0x1804,0x1002,0x402a,0x9156,0x70b1,0x68d1,0x6030,0x500d,0x480b,
  0x400b,0x400b,0x400b,0x400b,0x480c,0x480c,0x480c,0x480c,0x580e,0x500d,0x2807,0x0802,0x2005,0x3809,0x480b,0x480c,
  0x2006,0x2005,0x1804,0x2005,0x3809,0x480c,0x586e,0x480b,0x2005,0x2005,0x70d1,0x8114,0x584e,0x604f,0x580f,0x500d,
  0x500d,0x480c,0x480b,0x480b,0x500d,0x500d,0x582e,0x78f2,0x582e,0x3008,0x1003,0x1003,0x1804,0x2005,0x2005,0x2806,
  0x3849,0x3809,0x402b,0x68f0,0x89b5,0x9a17,0xa257,0x91f6,0x484b,0x1804,0x3809,0x8134,0x7092,0x480c,0x500d,0x500d,
  0x500d,0x500d,0x500d,0x500d,0x500d,0x70f1,0x91d6,0x68d0,0x2807,0x1804,0x3008,0x606f,0x70f1,0x58ae,0x508c,0x404a,
  0x8974,0x8154,0x9195,0xa237,0x99f6,0x91b5,0x99d6,0xb2b9,0x9215,0x3829,0x1804,0x484b,0x80f4,0x7091,0x502d,0x580e,
  0x582f,0x500d,0x580e,0x6030,0x8154,0x91d5,0x508d,0x2005,0x1002,0x3809,0x70b1,0x9a17,0x8134,0x8113,0x99f6,0x91b6,
  0x582e,0x604f,0x7112,0x91b5,0x7913,0x70d2,0x8974,0xaa98,0xa217,0x81b3,0x2806,0x2005,0x586e,0x9176,0x8994,0x584f,
  0x600f,0x6030,0x602f,0x8954,0x91b5,0x488b,0x1003,0x0802,0x1002,0x400b,0x99f7,0x8994,0x580e,0x582e,0x604f,0x602f,
  0x500d,0x586e,0x608f,0x68d0,0x68d0,0x68b1,0x606f,0x9a16,0x91d6,0x99f7,0x6950,0x1805,0x2807,0x584e,0x8114,0x91b5,
  0x7952,0x8133,0x91b5,0x7111,0x3028,0x1804,0x1805,0x2005,0x1003,0x3008,0x91f6,0x70b1,0x602f,0x604f,0x582e,0x500d,
  0x500d,0x500d,0x500d,0x502d,0x68b0,0x502d,0x480c,0x70f1,0x8193,0x7913,0x8994,0x3849,0x1003,0x2006,0x482c,0x68d0,
  0x7912,0x70d1,0x506c,0x2806,0x2806,0x3809,0x502c,0x400a,0x2806,0x1805,0x7131,0x70b1,0x604f,0x584f,0x582e,0x500d,
  0x500d,0x500d,0x500d,0x500c,0x480c,0x480c,0x480c,0x584d,0x7972,0x68d0,0x9195,0x7932,0x2005,0x1002,0x1003,0x1804,
  0x2005,0x1805,0x1804,0x3808,0x608f,0x78f3,0x91b6,0x8153,0x3809,0x2005,0x406b,0x606f,0x580e,0x604f,0x584e,0x500d,
  0x500d,0x500d,0x500d,0x500d,0x582e,0x582e,0x500d,0x500d,0x70f1,0x68d0,0x7912,0x99b6,0x506d,0x1804,0x0802,0x1805,
  0x2806,0x3008,0x60ae,0x8994,0x89b4,0x8193,0x6890,0x99f6,0x60af,0x2006,0x2806,0x480b,0x500d,0x580e,0x582e,0x500d,
  0x500d,0x500d,0x500d,0x502d,0x586e,0x606f,0x606f,0x606f,0x584e,0x582e,0x586e,0x8954,0x8113,0x3008,0x1003,0x3007,
  0x68b0,0x8133,0x70f1,0x586e,0x7111,0x68f0,0x500d,0x8153,0x8954,0x3008,0x1804,0x2006,0x3008,0x480b,0x500d,0x500d,
  0x500d,0x500d,0x500c,0x480c,0x502d,0x584e,0x502d,0x502d,0x480c,0x480b,0x504d,0x70d1,0x8934,0x504d,0x2005,0x3008,
  0x91b5,0x6870,0x480c,0x480c,0x502d,0x480c,0x480c,0x586e,0x7953,0x3829,0x1003,0x1003,0x1804,0x2005,0x400a,0x500d,
  0x500d,0x500d,0x480c,0x480b,0x400b,0x482c,0x584d,0x480c,0x480c,0x500d,0x582e,0x608f,0x8113,0x68f0,0x2005,0x3027,
  0x8193,0x502d,0x480b,0x480c,0x584e,0x582e,0x582e,0x500d,0x7912,0x484b,0x1003,0x508c,0x58ae,0x3007,0x2005,0x3809,
  0x400a,0x500d,0x500d,0x500d,0x480c,0x480b,0x480c,0x480c,0x480c,0x500c,0x582e,0x60af,0x70d1,0x7111,0x2807,0x2806,
  0x70f1,0x502d,0x480c,0x480c,0x504d,0x502d,0x500d,0x500d,0x70d1,0x484c,0x1804,0x58ad,0x6890,0x480c,0x3008,0x2005,
  0x2006,0x480c,0x500d,0x500d,0x500d,0x480c,0x480c,0x480c,0x480c,0x480c,0x584e,0x60af,0x604f,0x70f1,0x3007,0x2806,
  0x582f,0x500d,0x502e,0x584e,0x502d,0x480c,0x480c,0x480c,0x70b1,0x506d,0x2005,0x400b,0x500d,0x500d,0x400b,0x3007,
  0x2006,0x3007,0x500d,0x500d,0x500d,0x500d,0x500d,0x500d,0x500d,0x500d,0x606f,0x584e,0x582e,0x68b0,0x3007,0x2806,
  0x500d,0x500d,0x500d,0x500d,0x500d,0x500d,0x500d,0x500d,0x6871,0x482b,0x2005,0x400b,0x500d,0x500d,0x68d0,0x402a,
  0x3007,0x2006,0x480b,0x500d,0x500d,0x500d,0x500d,0x500d,0x500d,0x586e,0x7111,0x500d,0x582e,0x586d,0x2005,0x2006,
  0x500d,0x500d,0x500d,0x500d,0x582e,0x580e,0x500d,0x602f,0x6871,0x3007,0x2006,0x400b,0x500d,0x500d,0x6890,0x6930,
};

// -----------------------------------------------
int16_t numVerts = numVerts1;
int16_t *verts   = (int16_t*)verts1;
int16_t numQuads = numQuads1;
int16_t *quads   = (int16_t*)quads1;
uint16_t *quadColor = (uint16_t*)quadColor1;

#define MAXQUADS 6*20
#define MAXVERTS 8*20

int transVerts[MAXVERTS*3];
int projVerts[MAXVERTS*2];
int sortedQuads[MAXQUADS];
int rot0 = 0, rot1 = 0;
int numVisible = 0;

// simple Amiga-like blitter implementation
void rasterize(int x0, int y0, int x1, int y1, int *line)
{
  if((y0<yFr && y1<yFr) || (y0>=yFr+NLINES && y1>=yFr+NLINES)) return; // exit if line outside rasterized area
  int dx = abs(x1 - x0);
  int dy = abs(y1 - y0);
  int err2,err = dx-dy;
  int sx = (x0 < x1) ? 1 : -1;
  int sy = (y0 < y1) ? 1 : -1;

  while(1)  {
    if(y0>=yFr && y0<yFr+NLINES) {
      if(x0<line[2*(y0-yFr)+0]) line[2*(y0-yFr)+0] = x0>0 ? x0 : 0;
      if(x0>line[2*(y0-yFr)+1]) line[2*(y0-yFr)+1] = x0<WD_3D ? x0 : WD_3D-1;
    }

    if(x0==x1 && y0==y1)  return;
    err2 = err+err;
    if(err2 > -dy) { err -= dy; x0 += sx; }
    if(err2 < dx)  { err += dx; y0 += sy; }
  }
}

void drawQuad( int x0, int y0, int x1, int y1, int x2, int y2, int x3, int y3, uint16_t c)
{
  int x,y;
  int line[NLINES*2];
  for(y=0;y<NLINES;y++) { line[2*y+0] = WD_3D+1; line[2*y+1] = -1; }

  rasterize( x0, y0, x1, y1, line );
  rasterize( x1, y1, x2, y2, line );
  rasterize( x2, y2, x3, y3, line );
  rasterize( x3, y3, x0, y0, line );

  for(y=0;y<NLINES;y++)
    if(line[2*y+1]>line[2*y+0]) for(x=line[2*y+0]; x<=line[2*y+1]; x++) frBuf[SCR_WD*y+x]=c;
}

void cullQuads(int *v)
{
  // backface culling
  numVisible=0;
  for(int i=0;i<numQuads;i++) {
    if(bfCull) {
      int x1 = v[3*quads[4*i+0]+0]-v[3*quads[4*i+1]+0];
      int y1 = v[3*quads[4*i+0]+1]-v[3*quads[4*i+1]+1];
      int x2 = v[3*quads[4*i+2]+0]-v[3*quads[4*i+1]+0];
      int y2 = v[3*quads[4*i+2]+1]-v[3*quads[4*i+1]+1];
      int z = x1*y2-y1*x2;
      if(z<0) sortedQuads[numVisible++] = i;
    } else sortedQuads[numVisible++] = i;
  }

  int i,j,zPoly[numVisible];
  // average Z of the polygon
  for(i=0;i<numVisible;++i) {
    zPoly[i] = 0;
    for(j=0;j<4;++j) zPoly[i] += v[3*quads[4*sortedQuads[i]+j]+2];
  }

  // sort by Z
  for(i=0;i<numVisible-1;++i) {
    for(j=i;j<numVisible;++j) {
      if(zPoly[i]<zPoly[j]) {
        SWAP_INT(zPoly[j],zPoly[i]);
        SWAP_INT(sortedQuads[j],sortedQuads[i]);
      }
    }
  }
}

void drawQuads(int *v2d)
{
  for(int i=0;i<numVisible;i++) {
    int q = sortedQuads[i];
    int v0 = quads[4*q+0];
    int v1 = quads[4*q+1];
    int v2 = quads[4*q+2];
    int v3 = quads[4*q+3];
    drawQuad( v2d[2*v0+0],v2d[2*v0+1],  v2d[2*v1+0],v2d[2*v1+1], v2d[2*v2+0],v2d[2*v2+1], v2d[2*v3+0],v2d[2*v3+1], quadColor[q]);
  }
}

// animated checkerboard pattern
void backgroundChecker(int i)
{
  int x,y,xx,yy, xo,yo;
  xo = 25*fastSin(4*i)/256+50;
  yo = 25*fastSin(5*i)/256+50+yFr;

  for(y=0;y<NLINES;y++) {
    yy = (50+y+yo+yFr)% 32;
    for(x=0;x<WD_3D;x++) {
      xx = (50+x+xo)% 32;
      frBuf[SCR_WD*y+x] = ((xx<16 && yy<16) || (xx>16 && yy>16)) ? RGB(40,40,20) : RGB(80,80,40);
    }
  }
}

void backgroundPattern(int i, const unsigned short *pat)
{
  int x,y,xp,yp;
  xp = 25*fastSin(4*i)/256+50;  // 256 not MAXSIN=255 to avoid jumping at max sin value
  yp = 25*fastSin(5*i)/256+50+yFr;
  for(y=0;y<NLINES;y++) for(x=0;x<WD_3D;x++) frBuf[SCR_WD*y+x] = pat[((y+yp)&0x1f)*32 + ((x+xp)&0x1f)];
}

// ------------------------------------------------

typedef struct{
  int16_t x,y,z;
  int16_t x2d,y2d, x2dOld,y2dOld;
}Star;

#define NUM_STARS 150
Star stars[NUM_STARS];
int starSpeed = 20;

int rand3d(int min, int max)
{
   return min + rand() % (max+1 - min);
}

void initStar(int i)
{
  stars[i].x = rand3d(-500, 500);
  stars[i].y = rand3d(-500, 500);
  stars[i].z = rand3d(100, 2000);
  // remove stars from the center
  if(stars[i].x<80 && stars[i].x>-80) stars[i].x=80;
  if(stars[i].y<80 && stars[i].y>-80) stars[i].y=80;
}

int16_t rotZ = 1;

void updateStars()
{
  int16_t i,x,y;
  for(i=0; i<NUM_STARS; i++) {
    if(rotZ) {
      x = stars[i].x;
      y = stars[i].y;
      stars[i].x = (x*254 - y*2)/MAXSIN;
      stars[i].y = (y*254 + x*2)/MAXSIN;
    }

    stars[i].z -= starSpeed;
    stars[i].x2d = WD_3D/2 + 100 * stars[i].x / stars[i].z;
    stars[i].y2d = HT_3D/2 + 100 * stars[i].y / stars[i].z;

    if(stars[i].x2d>WD_3D || stars[i].x2d<0 || stars[i].y2d>HT_3D || stars[i].y2d<0) {
      initStar(i);
      stars[i].x2d = WD_3D/2 + 100 * stars[i].x / stars[i].z;
      stars[i].y2d = HT_3D/2 + 100 * stars[i].y / stars[i].z;
      stars[i].x2dOld = stars[i].x2d;
      stars[i].y2dOld = stars[i].y2d;
    }
  }
}

void initStars()
{
  for(int i=0; i<NUM_STARS; i++) initStar(i);
  updateStars();
  for(int i=0; i<NUM_STARS; i++) {
    stars[i].x2dOld = stars[i].x2d;
    stars[i].y2dOld = stars[i].y2d;
  }
}

void backgroundStars(int f)
{
  int i;
  for(i=0; i<NLINES*WD_3D; i++) frBuf[i] = CL_BLACK;
  for(i=0; i<NUM_STARS; i++) {
    int r = 255-stars[i].z/5;
    if(r>255) r=255;
    if(r<40) r=40;
    uint16_t col = RGB(r,r,r);
    int x = stars[i].x2d;
    int y = stars[i].y2d - yFr;
    if(x>=0 && x<WD_3D && y>0 && y<NLINES) frBuf[SCR_WD*y+x] = col;
  }
}

// ------------------------------------------------

int t=0;

void render3D(void)
{
  int cos0,sin0,cos1,sin1;
  int i,x0,y0,z0,fac,distToObj;
  int camZ = 200;
  int scaleFactor = HT_3D/4;
  int near = 300;

  if(t++>360) t-=360;
  distToObj = 150 + 300*fastSin(3*t)/MAXSIN;
  cos0 = fastCos(rot0);
  sin0 = fastSin(rot0);
  cos1 = fastCos(rot1);
  sin1 = fastSin(rot1);

  for(i=0;i<numVerts;i++) {
    x0 = verts[3*i+0];
    y0 = verts[3*i+1];
    z0 = verts[3*i+2];
    transVerts[3*i+0] = (cos0*x0 + sin0*z0)/MAXSIN;
    transVerts[3*i+1] = (cos1*y0 + (cos0*sin1*z0-sin0*sin1*x0)/MAXSIN)/MAXSIN;
    transVerts[3*i+2] = camZ + ((cos0*cos1*z0-sin0*cos1*x0)/MAXSIN - sin1*y0)/MAXSIN;

    fac = scaleFactor * near / (transVerts[3*i+2]+near+distToObj);

    projVerts[2*i+0] = (100*WD_3D/2 + fac*transVerts[3*i+0] + 100/2)/100;
    projVerts[2*i+1] = (100*HT_3D/2 + fac*transVerts[3*i+1] + 100/2)/100;
  }

  if(bgMode==3) updateStars();
  cullQuads(transVerts);

  tft.startWrite();
  for(i=0;i<HT_3D;i+=NLINES) {
    yFr = i;
    if(bgMode==0) backgroundPattern(t,pat2); else
    if(bgMode==1) backgroundPattern(t,pat8); else
    if(bgMode==2) backgroundPattern(t,pat7); else
    if(bgMode==3) backgroundStars(t); else
    if(bgMode==4) backgroundChecker(t);
    drawQuads(projVerts);

    // 原版STM32用DMA2D把frBuf搬到LCD的SDRAM显存,这里等价换成TFT_eSPI逐行推屏
    tft.pushImage(0, yFr, SCR_WD, NLINES, frBuf);
  }
  tft.endWrite();

  rot0 += 2;
  rot1 += 4;
  if(rot0>360) rot0-=360;
  if(rot1>360) rot1-=360;
}

// --------------------------------------------------------------------------

unsigned long lastSwitchMs = 0;
const unsigned long SWITCH_INTERVAL_MS = 6000; // 每6秒切一次背景+物体,跟原版摇杆/定时器切换效果对应

// 原版armfly例程里这段是注释掉的(用的是LCD_DispStr+FONT_T),这里用TFT_eSPI小字体在
// 左下角实机复刻:当前帧耗时/fps(黄) + 本轮最小-最大耗时/fps(绿) + 面片总数/可见数(品红)
unsigned int msMin = 1000, msMax = 0;

// TJpg_Decoder解码一块(w x h)就回调一次,直接怼给TFT_eSPI画出来,
// 跟厂商demo(6_1_SD_Jpg/SD_Jpg.ino)里的tft_output写法一样
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap)
{
  if (y >= tft.height()) return 0; // 超出屏幕就不用再画了,停止解码剩下的块
  tft.pushImage(x, y, w, h, bitmap);
  return 1;
}

// 视频帧是320x180,屏幕是320x240,上下各留30px黑边居中显示,不拉伸变形
void playVideoFrame()
{
  unsigned long now = millis();
  if (now - lastVideoFrameMs < (1000 / VIDEO_FPS)) return; // 按目标帧率节流,读卡+解码比这快就等着
  lastVideoFrameMs = now;

  char path[32];
  snprintf(path, sizeof(path), "/bbb/frame%04d.jpg", videoFrameIdx);
  int yOff = (SCR_HT - VIDEO_FRAME_H) / 2;
  TJpgDec.drawSdJpg(0, yOff, path);

  if (++videoFrameIdx > VIDEO_FRAME_COUNT) videoFrameIdx = 1;
}

// 只看IRQ脚有没有被拉低,不读坐标——点屏幕任意位置都能切换3D演示/SD视频这两个模式
void checkTouchToggle()
{
  bool down = (digitalRead(TOUCH_IRQ) == LOW);
  if (down && !touchWasDown && millis() - lastToggleMs > 400) {
    mode = 1 - mode;
    lastToggleMs = millis();
    tft.fillScreen(TFT_BLACK);
    if (mode == 1) videoFrameIdx = 1;
  }
  touchWasDown = down;
}

void run3DFrame()
{
  if(millis() - lastSwitchMs >= SWITCH_INTERVAL_MS) {
    lastSwitchMs = millis();
    if(++bgMode>4) bgMode=0;
    if(++object>3) object=0;
    msMin = 1000;
    msMax = 0;
  }

  switch(object) {
    case 0:
      numVerts  = numVerts1;
      verts     = (int16_t*)verts1;
      numQuads  = numQuads1;
      quads     = (int16_t*)quads1;
      quadColor = (uint16_t*)quadColor1;
      bfCull    = 1;
      break;
    case 1:
      numVerts  = numVerts2;
      verts     = (int16_t*)verts2;
      numQuads  = numQuads2;
      quads     = (int16_t*)quads2;
      quadColor = (uint16_t*)quadColor2;
      bfCull    = 1;
      break;
    case 2:
    default:
      numVerts  = numVerts3;
      verts     = (int16_t*)verts3;
      numQuads  = numQuads3;
      quads     = (int16_t*)quads3;
      quadColor = (uint16_t*)quadColor3;
      bfCull    = 1;
      break;
    case 3:
      numVerts  = numVerts4;
      verts     = (int16_t*)verts4;
      numQuads  = numQuads4;
      quads     = (int16_t*)quads4;
      quadColor = (uint16_t*)quadColor4;
      bfCull    = 0;
      break;
  }

  unsigned long ms = millis();
  render3D();
  ms = millis() - ms;
  if(ms<msMin) msMin=ms;
  if(ms>msMax) msMax=ms;

  char txt[40];
  snprintf(txt, sizeof(txt), "%lu ms     %lu fps ", ms, ms>0?1000/ms:0);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString(txt, 0, SCR_HT-26);

  snprintf(txt, sizeof(txt), "%u-%u ms  %u-%u fps   ", msMin, msMax, msMax>0?1000/msMax:0, msMin>0?1000/msMin:0);
  tft.setTextColor(TFT_GREEN, TFT_BLACK);
  tft.drawString(txt, 0, SCR_HT-18);

  snprintf(txt, sizeof(txt), "total/vis %d / %d   ", numQuads, numVisible);
  tft.setTextColor(TFT_MAGENTA, TFT_BLACK);
  tft.drawString(txt, 0, SCR_HT-10);

  static unsigned long lastPrintMs = 0;
  if(millis() - lastPrintMs >= 1000) {
    lastPrintMs = millis();
    Serial.printf("object=%d bgMode=%d  frame=%lums  ~%lufps  quads %d/%d\n",
                  object, bgMode, ms, ms>0?1000/ms:0, numVisible, numQuads);
  }
}

void setup()
{
  Serial.begin(115200);
  pinMode(TOUCH_IRQ, INPUT_PULLUP); // 空闲时IRQ被拉高,按下变低

  // SD要在TFT前面初始化,跟厂商demo顺序一致;SD卡没插/坏了也不阻塞3D演示,只是切不过去视频模式
  if (!SD.begin(SD_CS)) {
    Serial.println("SD.begin failed! (视频模式会用不了,3D演示不受影响)");
  } else {
    Serial.println("SD OK");
  }

  tft.begin();
  tft.setRotation(1);          // 320x240 横屏,跟同目录其它工程一致
  tft.setSwapBytes(true);      // 配合上面RGB()宏生成的颜色顺序,TJpg_Decoder也吃这个设置
  tft.fillScreen(TFT_BLACK);
  tft.setTextFont(1);
  tft.setTextSize(1);

  TJpgDec.setJpgScale(1);
  TJpgDec.setCallback(tft_output);

  initStars();
  lastSwitchMs = millis();
}

void loop()
{
  checkTouchToggle();
  if (mode == 0) run3DFrame();
  else playVideoFrame();
}

烧录方式

下载完整烧录包 ZIP,提供四种方式,按自己趁手的选一种就行:

Arduino IDE

打开 01_Arduino_INO 里的 .ino,板子选普通ESP32 Dev Module,直接编译上传。

PlatformIO

02_PlatformIO_Project 完整工程,pio run --target upload 一条命令搞定。

esptool命令行

03_BIN_Firmware 下已编译好的 .bin,不用装Arduino IDE,直接用esptool烧。

网页烧录器

打开网页烧录器(已实测),浏览器里点按钮就能烧,什么软件都不用装。

网页烧录器使用提醒:只支持电脑端Chrome/Edge浏览器(基于Web Serial API);客户直接从本站打开即可。本地离线测试时要起静态服务器打开(python -m http.server 8080),不能直接双击html文件。USB线必须是数据线,充电线没有数据针脚,电脑设备管理器里不会出现串口。

✓ 已实测通过:普通ESP32通过网页一键烧录merged_firmware_0x0.bin成功,写入437760字节,耗时约24.8秒。