しょーもない小ネタですがせっかく作ったので。
ちょっと前(2020年4月~?)から、TwitterやYouTubeで「虹色に光りながらぐるぐる踊る鳥」のGIFアニメや動画を見かけるようになりました。
↓こやつです。勝手にゲーミングインコと呼んでました。
注)結構ハデにLEDがピカピカしてる映像など出てきます。人によっては具合が悪くなる可能性があるので(いわゆるポケモンショック)、ご注意ください。
ゲーミングインコの正体
割とよく見かけるので気になって調べてみたら「Party Parrot」というインターネットミームで、エンジニアのチャットなどでよく出てくる絵文字のようなものなんだとか、絶滅危惧種のオウムの仲間が元ネタなんだとか。
色々なGIFアニメーションを集めたコミュニティサイトが作られているだとか、
そんな感じのやつでした。
私が「ゲーミング」と呼んでたのは単にゲーミングパソコンや関連パーツがLEDでハデハデにピカピカしてるイメージが強くてこのGIFとイメージが一致したからです。
別に何かのゲームに出てきたわけでもなければ、どこかのゲーミングパソコンにシールが貼ってあったとか、そういうのではないです。
で、
作ってみた
作ってみましたParty Parrot。
といっても物理的にぐるぐるするやつは先駆者がいることを知っていたので、
(こっちは私じゃないよ!)
最近流行のトリ作ってみた笑#PartyParrot pic.twitter.com/uT4nhf21dq
— おみょ (@omyopoja) May 2, 2020
私は液晶表示&ハデハデなフルカラーLEDで対抗!
なかなか動画にうまく撮れなくて苦労しました。
白飛びしまくるのでプログラムで照度を全開の1/5くらいに下げています。
フルパワーの実物はなかなかド派手にピカピカします。パーリータイムです。
遊び方
ベースにしたM5StackにはA、B、Cの3つのボタンがあります。
こんな風に機能を割り振りました。
- Aボタン短押し:B・Cボタンでのぐるぐるの繰り返し回数設定(1~10回、押すごとに+1)
- Aボタン長押し:LEDの点灯モード切替(1 or 2)
- Bボタン:Party!!(液晶アニメーション)
- Cボタン:Party!!(液晶アニメーション+LED)
繰り返し回数とLEDモードは切替後に画面の左上に小さく表示されます。(パーリィすると消えます)
LEDの点灯モードはこんな感じ。M5Stackの各辺に5個ずつ、計20個配置しました。
- モード1:全部同じ色で点灯。色はそのコマでのカカポの色と同じ。
- モード2:すべての色を一度に表示して、ぐるぐる回転させる。
ちなみに、
全部で10フレームあります。
(2フレーム目の白がオレンジでもよさそう…と思ってよく調べてみたらコミュニティサイトの一番上のやつと微妙に違う…どうやら派生ファイルをベースにしてしまったみたいです。そのうち作り直します。)
パールィーしてないときは一番最初の赤カカポを表示しておきます。
技術的な話
とはいえM5Stackが使えれば簡単なことです。
①バラバラの画像を順に表示してアニメーションさせ、任意の画像で停止させる
②アドレサブルフルカラーLEDを制御する
③ボタンで設定を変える・ボタンをパーティーのトリガーにする
これだけです。が、結構なハマりポイントがありました。
まず、
jpeg画像の表示が遅いんじゃ!
SDカードに保存した画像を直接読み込むとこんなスピード感でした。
リフレッシュレートの感じをGIFアニメで再現。(実測で1フレーム≒0.16秒でしたがGIFアニメは0.2秒になってます)
これじゃお話になりませぬ。カクカクです。
この時点でのスケッチ(M5用のサンプルスケッチ「JpegDraw.ino」をもとに作成)が、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <M5Stack.h> void setup(void) { M5.begin(); M5.Lcd.setBrightness(200); } void loop() { M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp01.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp02.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp03.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp04.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp05.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp06.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp07.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp08.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp09.jpg"); M5.Lcd.drawJpgFile(SD, "/GamingKakapo/pp10.jpg"); } |
これ、考えうる最速のスケッチのはずです…よね?
画像の表示以外の処理が一切ない状態でこんな状態なので、ここにLEDやボタンの処理を入れたら…もはやぜつぼうしかない。
救世主現る
lovyan03さんという方が作成・配布してくださっている「JpgLoopAnime.ino」を使うと、高速で表示できました。
SDカードの画像をM5Stack(のESP32)にメモリに一度全部読みだして、ESP32のデュアルコアの活用、DMA転送によって表示することで高速表示できるというものです。
ただ、GIFアニメーションを分解した個別の「アニメーションしない」GIFファイルをjpegに変換してSDカードに入れてみたところ、
こんなコトに。10枚も読み込むにはメモリが足りなかった?(これもGIFアニメで無理やり再現)
もともと100%に設定していた変換ソフトの品質を20%まで下げれば全コマ表示できますが今度は、
なんかきちゃないことに。どうしよう。
(パソコンやスマホではきれいに見えますが、M5Stackの液晶ではノイズが目立ってなかなか悲惨な感じでした)
…
こうしました。
最初の赤カカポは常に表示しているので低品質ではアラが目立ちます。
一方で、それ以外の9カカポが表示されるのは0.何秒の単位。
そこで、「赤カカポは品質100%で、それ以外のカカポは20%で」と変換品質を変えてみたところ、それなりにうまく表示できた次第。
ファイルの容量的には赤カカポが約42kB、それ以外は約5kBです。
LED付きボトムモジュールを作る
M5GOやM5FireにはボトムモジュールにNeoPixelLEDがついていますが、あいにく手元にあるのは無印とGray。
ならば、NeoPixelLEDのついたボトムモジュールを作ってしまえと、いつものパターンです。
既存のボトムモジュールを参考に筐体をFusion360で設計。
この時点でM5Stackと使用するLEDモジュールのサイズを見て一辺に5個と決めました。
LEDを思いっきり外側に向けることでとにかく明るく、とにかくハデに見せます(笑)
本来のボトムモジュールにある、バッテリーやGPIOの接続コネクタは省略。
本体モジュールとの接続部分にはM5Stack専用のユニバーサル基板とピンヘッダを使用します。
3Dプリント。透明PLAフィラメントを使いました。(写真は全部組み込み後ですが…)
とても「透明」とは言えない仕上がりですが、この半透明感が良い感じにLEDの光を拡散しつつ、ム〇カ大佐状態(目がー目がー!(まぶしい!))になるのを防いでくれることを期待。
さすがにLEDからダイレクトはあかんて。
下に置いているのは使用したNeoPixelLEDのモジュール。このモジュールを5個単位で折って使用しました。
特に設計段階で考えていたわけではないのですが、LEDモジュール5個分の幅(正確には基板をちょっと削ってますが…)と、筐体の内寸がシンデレラフィットしてびっくりしました。
コーナー部分でのモジュールの接続はモジュールを押し込んでから端子を直接はんだ付けするだけで済みました。
設計段階で電線の取り回しを悩みつつ、まあ柔らかい線をうまいこと押し込めばいいか、と思っていたらまさかの直接はんだ付けでOKだったという。(なお、後からLEDの不良が発覚して交換大変でした…)
電気回路的にはVCCを3.3Vに、GNDはGNDに、NeoPixelの信号ピンはG2にそれぞれ直接はんだ付けしてるだけです。なので回路図は省略。
スケッチ
いろいろ突っ込みどころがありそうですが置いておきます。
何気にSD Updaterに対応しております。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | /*----------------------------------------------------------------------------/ M5Stack JpgLoopAnime Original Source: https://github.com/lovyan03/M5Stack_JpgLoopAnime/ Licence: [MIT](https://github.com/lovyan03/M5Stack_JpgLoopAnime/blob/master/LICENSE) Author: [lovyan03](https://twitter.com/lovyan03) /----------------------------------------------------------------------------*/ #include <M5Stack.h> #include <M5StackUpdater.h> // https://github.com/tobozo/M5Stack-SD-Updater/ #include <esp_heap_caps.h> #include <vector> #include "src/MainClass.h" #include "src/images.h" #include <FastLED.h> static LGFX lcd; static MainClass main; #define NUM_LEDS 20 #define DATA_PIN 2 CRGB leds[NUM_LEDS]; // ここで画像ファイルのディレクトリ名を指定する std::vector<String> imageDirs = {"/GamingKakapo", "/image_dirname2"}; //LED色 int colors[11][3] = {{255, 55, 56}, {255,255,255}, {255,113, 76}, { 72,255, 75}, { 73,255,255}, { 72, 95,255}, {220, 73,255}, {255, 73,255}, {255, 54,255}, {255, 54, 96}, { 0, 0, 0}}; int LEDStart = 0; int LEDBright = 255; uint_fast8_t dirIndex = 0; std::vector<const uint8_t*> fbuf; std::vector<int32_t> fbufsize; uint32_t fpsCount = 0, fpsSec = 0; bool loadImages(const String& path){ bool res = false; fbuf.clear(); fbufsize.clear(); Serial.println(path); File root = SD.open(path); File file = root.openNextFile(); uint8_t* tmp; while (file){ tmp = (uint8_t*)heap_caps_malloc(file.size(), MALLOC_CAP_DEFAULT); if (tmp) { file.read(tmp, file.size()); fbufsize.push_back(file.size()); fbuf.push_back(tmp); res = true; } file = root.openNextFile(); } return res; } void GamingKakapo(boolean LED,int Repeat,int ModeLed){ for(int h=0;h<Repeat;h++){ for(int i=1;i<fbuf.size();i++){ main.drawJpg(fbuf[i], fbufsize[i]); if(LED==true){ //LED制御 switch(ModeLed){ case 1: //全体同じ色 for(int j=0;j<NUM_LEDS;j++) leds[j] = CRGB(colors[i][0],colors[i][1],colors[i][2]); break; case 2: //カラフルぐるぐる for(int j=0;j<NUM_LEDS;j++){ int k=j+LEDStart; if(j>NUM_LEDS) j-=NUM_LEDS; leds[NUM_LEDS-j-1] = CRGB(colors[k%10][0],colors[k%10][1],colors[k%10][2]); } break; } }else{ //LED消灯 for(int j=0;j<NUM_LEDS;j++) leds[j] = CRGB(colors[10][0],colors[10][1],colors[10][2]); } LEDStart++; FastLED.show(); delay(20); } main.drawJpg(fbuf[0], fbufsize[0]); if(LED==true){ if(ModeLed==1){ //LED制御モード1の時は最初の赤に、モード2の時は最後のカラフル表示のまま for(int j=0;j<NUM_LEDS;j++) leds[j] = CRGB(colors[0][0],colors[0][1],colors[0][2]); }else if(ModeLed==2){ for(int j=0;j<NUM_LEDS;j++){ int k=j+LEDStart; if(j>NUM_LEDS) j-=NUM_LEDS; leds[NUM_LEDS-j-1] = CRGB(colors[k%10][0],colors[k%10][1],colors[k%10][2]); } } FastLED.show(); } if(LED==false){ //LED消灯 for(int j=0;j<NUM_LEDS;j++) leds[j] = CRGB(colors[10][0],colors[10][1],colors[10][2]); } delay(10); } if(LED==true){ for(int j=LEDBright;j>0;j--){ FastLED.setBrightness(j); FastLED.show(); delay(2); } for(int j=0;j<NUM_LEDS;j++) leds[j] = CRGB(colors[10][0],colors[10][1],colors[10][2]); FastLED.setBrightness(LEDBright); FastLED.show(); } } void setup() { M5.begin(); #if defined ( __M5STACKUPDATER_H ) #ifdef __M5STACKUPDATER_H if(digitalRead(BUTTON_A_PIN) == 0) { Serial.println("Will Load menu binary"); updateFromFS(SD); ESP.restart(); } #endif #endif M5.Speaker.begin(); M5.Speaker.mute(); lcd.begin(); main.setup(&lcd); loadImages(imageDirs[dirIndex]); lcd.startWrite(); main.drawJpg(fbuf[0], fbufsize[0]); FastLED.addLeds<WS2812, DATA_PIN, GRB>(leds, NUM_LEDS); FastLED.setBrightness(50); for(int j=0;j<NUM_LEDS;j++) leds[j] = CRGB(colors[10][0],colors[10][1],colors[10][2]); FastLED.show(); for(int j=0;j<NUM_LEDS;j++) leds[j] = CRGB(colors[10][0],colors[10][1],colors[10][2]); FastLED.show(); Lcd.setTextColor(BLACK,WHITE); } void loop() { static int LEDMode = 1; static int repeatTime = 1; boolean BtnALong = false; M5.update(); //Aボタン長押し:LEDモード1切り替え if(M5.BtnA.pressedFor(1000)){ BtnALong = true; LEDMode++; if(LEDMode>2) LEDMode=1; Lcd.setCursor(1,1,1); Lcd.printf("LEDMode:%d ",LEDMode); while(!M5.BtnA.wasReleased()){ M5.update(); } } //Aボタン短推し:繰り返し回数 if(M5.BtnA.wasReleased()){ if(BtnALong==true){ BtnALong = false; }else{ repeatTime++; if(repeatTime>10) repeatTime=1; Lcd.setCursor(1,1,1); Lcd.printf("Repeat:%d ",repeatTime); } } //Bボタン:ゲーミングカカポ(液晶だけ) if (M5.BtnB.isPressed()){ GamingKakapo(false,repeatTime,0); } //Cボタン:ゲーミングカカポ(液晶+LEDモード2(グラデーションでぐるぐる)) if (M5.BtnC.isPressed()){ LEDStart = 0; GamingKakapo(true,repeatTime,LEDMode); } } |
ということで思い付きで作ったしょーもないネタのご紹介でした。
側面がアクリルでできていて中が見えるゲーミングパソコンのケースの中に置いたら面白いかもしれません。電源USBで取れるし。
何かパソコンの操作やディスクアクセス、SNSの通知がパーリーのトリガーになるようにして、とか。
LEDモジュールはほかにも「色と光で何かを表示する」ような用途に広く使えそうです。
明るいモジュールを暗くして使うのは簡単なので。(逆は大変ですけど)
コメント