20数年前、当時のパソコン上でVisualBASIC6.0を使いフラクタルやカオスを描いていました。現在、ディスプレイモジュールも65Kカラーで画素数が480x320に達し、マイコンの演算も速くノートPCとさほど変わらぬ環境になったので、マンデルブロー集合をやってみようと思いました。使用したのは次。
・ESP32(TTGO LoRa32ーOLED. Arduino IDEでのボード名)
・3.5" TFT SPI 480x320 V1.0 (IlI9488, XPT2046, with SD card slot)
・TFT_eSPI ライブラリ(タッチ機能で目標地点を拡大表示しようと当初は考えていた。LovyanGFXではタッチ機能がうまくセットアップできなかったのでこちらにした)
ESP32とディスプレイモジュールのSPIでの接続は以下の通り。
ESP32 | ディスプレイ部 | タッチパネル部(不使用ですが) |
---|---|---|
4 | T_CS | |
5 | CS | |
18 | SCK | T_CLK |
23 | SDI(MOSI) | T_DIN |
19 | T_DO | |
27 | DC/RS | |
32 or RST | RESET | |
3V3 | LED | |
GND | GND | |
3V3 | VCC |
"RGB 65K color"が使えとても綺麗です。なので昔のように8色や16色ではなく、ほぼフルカラーかつグラデーションで色を付けることにします。さらに静止画を表示するだけではつまらないので、PCでも出来なかった(私のPCと技術では)動きを付けてみようと思いました。そこで、発散判定する反復上限回数を少しずつ増やすことを考えました。例えば
1回目・・・120回反復
abs(z)>4.0→発散→120回中何回目だったかでpixel色付け
発散せず→pixelを黒
2回目・・・140回反復
abs(z)>4.0→発散→140回中何回目だったかでpixel色付け
発散せず→pixelを黒
・・・
しかし問題なのは、画像1枚の表示に現状20秒ほど要します。少しでも高速化するにはどうしよう? 考えたのは、
既に発散判定されている座標(Z、pixel、黒以外の色で塗られている)は計算せず飛ばし、黒のpixel(座標、Z)のみ計算する
ことにしました。これで少し速くなったような気が…(?)。しかし、どうも働いていないよう。次は、SPIの動作速度を上げる。つまり
#define SPI_FREQUENCY 40000000
としたら、1枚表示8秒程に改善され実用的になりました。
反復回数120~500までの成長(?)するマンデルブロー集合を20枚表示させると132秒程になりました。(80MHzでも動作しましたが、速度的にほとんど変わらぬ上、画面が少し乱れました。)
稚拙な作品ですが、ほぼ黒の画面の中にカラフルな画像が浮かび上がってくる様子に満足できました。
なお、風景の美しい場所を探すのに手間が掛かりました。PC上の昔の自分のプログラムを使い、良さそうな場所をピックアップしました。他にも変化の美しい場所は探せば一杯あるはずです。
またやる気が出たら、ジュリア集合などの他のフラクタル画像やカオス図形もやってみようかと思います。以下にArduinoIDE 2.1.1でのスケッチを示します。
【追記 投稿後に "tft.readPixel(int x,int y)" が使えることが解ったので記しておきます。SPI読込みの周波数を次のように20MHzから10MHzに落としたところ、Pixelの色を読取れるようになりました。しかし、そうすると読込みに時間を取られ反って動作速度が落ちたので実用的ではありませんでした。】
// Optional reduced SPI frequency for reading TFT
#define SPI_READ_FREQUENCY 10000000 // 20000000 in default
#include <TFT_eSPI.h>
// 必要なPin設定は libraries/TFT_eSPI/User_Setup.hで行う
// SPI_Speed Up 20MHz(in default) to 40MHz
// #define SPI_FREQUENCY 40000000 in about 360th line
TFT_eSPI tft = TFT_eSPI();// also work with RST pin -> board's RST
static uint16_t col[]{
0x0000, //0
0x222A, //1
0x867E, //2LightSkyBlue
0xF81F, //3Magenta
0x480F, //4Indigo
0xFF1B, //5MistyRose
0xAEBC, //6LightBlue
0xFFFB, //7LightYellow
0x07FF, //8Cyan
0xF800, //9Red
0x07E0, //10Green
0x001F, //11Blue
0xFCEE, //12LightSalmon
0xEF31, //13Khaki
0xDFFF, //14LightCyan
0xFFE0 //15Yellow
};
int X0 = 240;// tft.width()/2
int Y0 = 160;// tft.height()/2
float view; // viewport width
float di; // real/imaginary calculation step
float CtoX, CtoY; // 現在の表示(view)の中心
int repeat, k; // 反復上限、回数
int re, gr, bl; // color 0-255
float x, y, zr, zi, R, I;//複素計算変数
float absZ = 4.0; // 発散判定
int page, dp;
float toRad = PI / 180.0;//度をラジアンに変換
//PI=3.1415926535897932384626433832795 defined in Libraly
void setup() {
Serial.begin(115200);
Serial.println("begin");
tft.init();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
//tft.setTextColor(TFT_WHITE);
// default Mandelbrot Set -----------------------------------------
//view=2.4; CtoX = 0.0; CtoY = 0.0; repeat=200; page=1; dp=0;
// view = 0.00024; // 10000倍に拡大
// Viewport width; Viewport Center x;y; 反復上限; 画像枚数; 反復上限増分
// 他の風景の良い場所
view=0.00096; CtoX = -1.19998; CtoY = -0.15507; repeat=120; page=20; dp=2;
//view=0.00024; CtoX = -1.24997; CtoY = -0.00877; repeat=190; page=28; dp=2;
//view = 0.00024; CtoX=-1.401;CtoY=-0.000178; repeat=300; page=19; dp=2;
di = view / 480.0; // 1 pixel in Real and Imaginary number
unsigned long now=0;
unsigned long starttime;
starttime=millis();
for (int i = 1; i <= page; i++) {// 反復数上限を増やしていく
Mandelbrot();
now = millis();
tft.setCursor(0,312);
tft.printf("%2dpage:%3ds",i,now/1000);
repeat += dp*i;
}
grid();
tft.drawString("END",0,0);
}
void Mandelbrot(void) {
for (int j = 320- Y0; j >= - Y0; j--) {
y = j * di + CtoY;
for (int i = - X0; i < 480 - X0; i++) {
//if (pixel = tft.readPixel(X0 + i, 320 - Y0 - j) == 0) { // まだ発散してない点のみ計算
x = i * di + CtoX;
zr = 0.0;
zi = 0.0;
k = -1;
do {
k++;
R = zr * zr - zi * zi + x;
I = 2 * zr * zi + y;
zr = R;
zi = I;
if (k >= repeat) break;
} while ((R * R + I * I) < absZ);
if (k == repeat) tft.drawPixel(X0 + i, 320 - Y0 - j, TFT_BLACK);
else tft.drawPixel(X0 + i, 320 - Y0 - j, ColorSel(k));
//}
}
}
}
void grid(void) {
tft.drawLine(X0 + 100, 0, X0 + 100, tft.height(), 0xFFFF);
tft.drawLine(X0 - 100, 0, X0 - 100, tft.height(), 0xFFFF);
tft.drawLine(X0 + 200, 0, X0 + 200, tft.height(), 0xFFFF);
tft.drawLine(X0 - 200, 0, X0 - 200, tft.height(), 0xFFFF);
tft.drawLine(0, Y0 + 100, tft.width(), Y0 + 100, 0xFFFF);
tft.drawLine(0, Y0 - 100, tft.width(), Y0 - 100, 0xFFFF);
tft.drawLine(X0, 0, X0, tft.height(), TFT_RED); // 赤で垂直の線を描画
tft.drawLine(0, Y0, tft.width(), Y0, TFT_RED);
tft.drawString("1.0",X0+201,0);
tft.drawString("0",X0+1,0);
tft.drawString("-1.0",X0-199,0);
}
uint16_t ColorSel(int c) {
if (c < 16) {
return col[c];
} else {
re = 128 + 126 * cos(c * toRad);
gr = 128 + 126 * cos((c + 120) * toRad);
bl = 128 + 126 * cos((c + 240) * toRad);
return tft.color565(re, gr, bl);
}
}
void loop(void) {
}