MacでStackChan物語 #6 スタックチャンの顔の正体と顔を変更する方法
スタックチャンはもともと可愛らしいお顔をしているのですが、
自分好みの顔に変更しようと思います。
スタックチャンの顔の仕組みを学ぶ
Macのターミナルで、StackChanファームウェアのフォルダに移動してから、
find . -iname "*avatar*" -o -iname "*face*" -o -iname "*emotion*"を実行して、その顔の元になっているアバターを探します。
satoshi@Satoshi-MacBook-Pro xiaozhi-server % cd ~/git/StackChan/firmware
satoshi@Satoshi-MacBook-Pro firmware % find . -iname "*avatar*" -o -iname "*face*" -o -iname "*emotion*"
./managed_components/wvirgil123__sscma_client/interface
./managed_components/wvirgil123__sscma_client/interface/sscma_client_flasher_interface.h
./managed_components/wvirgil123__sscma_client/interface/sscma_client_io_interface.h
./managed_components/espressif__esp_codec_dev/interface
./managed_components/espressif__esp_lcd_st7796/priv_include/esp_lcd_st7796_interface.h
./managed_components/78__esp-ml307/include/network_interface.h
./managed_components/espressif__esp_lcd_st7701/priv_include/esp_lcd_st7701_interface.h
./managed_components/lvgl__lvgl/docs/src/details/xml/editor/user_interface.rst
./managed_components/lvgl__lvgl/docs/src/_static/images/editor_interface.png
./managed_components/lvgl__lvgl/demos/benchmark/assets/img_benchmark_avatar.png
./managed_components/lvgl__lvgl/demos/benchmark/assets/img_benchmark_avatar.c
./managed_components/lvgl__lvgl/demos/vector_graphic/assets/img_demo_vector_avatar.c
./managed_components/lvgl__lvgl/demos/vector_graphic/assets/avatar.png
./managed_components/lvgl__lvgl/demos/widgets/assets/avatar.png
./managed_components/lvgl__lvgl/demos/widgets/assets/img_demo_widgets_avatar.c
./managed_components/espressif__iot_eth/interface
./managed_components/espressif__iot_eth/interface/iot_eth_interface.h
./managed_components/espressif__button/interface
./managed_components/espressif__button/interface/button_interface.h
./managed_components/espressif__esp_sccb_intf/interface
./managed_components/espressif__esp_sccb_intf/interface/esp_sccb_io_interface.h
./managed_components/espressif__esp_sccb_intf/test_apps/dummy/main/test_interface.c
./managed_components/espressif__led_strip/interface
./managed_components/espressif__led_strip/interface/led_strip_interface.h
./managed_components/espressif__esp_lcd_co5300/priv_include/esp_lcd_co5300_interface.h
./managed_components/espressif__esp-sr/include/esp32c5/esp_afe_sr_iface.h
./managed_components/espressif__esp-sr/include/esp32c5/esp_vadn_iface.h
./managed_components/espressif__esp-sr/include/esp32c5/esp_mn_iface.h
./managed_components/espressif__esp-sr/include/esp32c5/esp_mfcc_iface.h
./managed_components/espressif__esp-sr/include/esp32c5/esp_wn_iface.h
./managed_components/espressif__esp-sr/include/esp32c5/esp_nsn_iface.h
./managed_components/espressif__esp-sr/include/esp32c3/esp_afe_sr_iface.h
./managed_components/espressif__esp-sr/include/esp32c3/esp_vadn_iface.h
./managed_components/espressif__esp-sr/include/esp32c3/esp_mn_iface.h
./managed_components/espressif__esp-sr/include/esp32c3/esp_mfcc_iface.h
./managed_components/espressif__esp-sr/include/esp32c3/esp_wn_iface.h
./managed_components/espressif__esp-sr/include/esp32c3/esp_nsn_iface.h
./managed_components/espressif__esp-sr/include/esp32p4_eco5/esp_afe_sr_iface.h
./managed_components/espressif__esp-sr/include/esp32p4_eco5/esp_vadn_iface.h
./managed_components/espressif__esp-sr/include/esp32p4_eco5/esp_mn_iface.h
./managed_components/espressif__esp-sr/include/esp32p4_eco5/esp_mfcc_iface.h
./managed_components/espressif__esp-sr/include/esp32p4_eco5/esp_wn_iface.h
./managed_components/espressif__esp-sr/include/esp32p4_eco5/esp_nsn_iface.h
./managed_components/espressif__esp-sr/include/esp32/esp_afe_sr_iface.h
./managed_components/espressif__esp-sr/include/esp32/esp_vadn_iface.h
./managed_components/espressif__esp-sr/include/esp32/esp_mn_iface.h
./managed_components/espressif__esp-sr/include/esp32/esp_mfcc_iface.h
./managed_components/espressif__esp-sr/include/esp32/esp_wn_iface.h
./managed_components/espressif__esp-sr/include/esp32/esp_nsn_iface.h
./managed_components/espressif__esp-sr/include/esp32c6/esp_afe_sr_iface.h
./managed_components/espressif__esp-sr/include/esp32c6/esp_vadn_iface.h
./managed_components/espressif__esp-sr/include/esp32c6/esp_mn_iface.h
./managed_components/espressif__esp-sr/include/esp32c6/esp_mfcc_iface.h
./managed_components/espressif__esp-sr/include/esp32c6/esp_wn_iface.h
./managed_components/espressif__esp-sr/include/esp32c6/esp_nsn_iface.h
./managed_components/espressif__esp-sr/include/esp32s3/esp_afe_sr_iface.h
./managed_components/espressif__esp-sr/include/esp32s3/esp_vadn_iface.h
./managed_components/espressif__esp-sr/include/esp32s3/esp_mn_iface.h
./managed_components/espressif__esp-sr/include/esp32s3/esp_mfcc_iface.h
./managed_components/espressif__esp-sr/include/esp32s3/esp_wn_iface.h
./managed_components/espressif__esp-sr/include/esp32s3/esp_nsn_iface.h
./managed_components/espressif__esp-sr/include/esp32s2/esp_afe_sr_iface.h
./managed_components/espressif__esp-sr/include/esp32s2/esp_vadn_iface.h
./managed_components/espressif__esp-sr/include/esp32s2/esp_mn_iface.h
./managed_components/espressif__esp-sr/include/esp32s2/esp_mfcc_iface.h
./managed_components/espressif__esp-sr/include/esp32s2/esp_wn_iface.h
./managed_components/espressif__esp-sr/include/esp32s2/esp_nsn_iface.h
./managed_components/espressif__esp-sr/include/esp32p4/esp_afe_sr_iface.h
./managed_components/espressif__esp-sr/include/esp32p4/esp_vadn_iface.h
./managed_components/espressif__esp-sr/include/esp32p4/esp_mn_iface.h
./managed_components/espressif__esp-sr/include/esp32p4/esp_mfcc_iface.h
./managed_components/espressif__esp-sr/include/esp32p4/esp_wn_iface.h
./managed_components/espressif__esp-sr/include/esp32p4/esp_nsn_iface.h
./build/esp-idf/main/CMakeFiles/__idf_main.dir/hal/hal_ws_avatar.cpp.obj
./build/esp-idf/main/CMakeFiles/__idf_main.dir/stackchan/avatar
./build/esp-idf/main/CMakeFiles/__idf_main.dir/apps/app_avatar
./build/esp-idf/main/CMakeFiles/__idf_main.dir/apps/app_avatar/app_avatar.cpp.obj
./main/hal/hal_ws_avatar.cpp
./main/stackchan/avatar
./main/stackchan/avatar/avatar.h
./main/stackchan/avatar/avatar
./main/stackchan/avatar/avatar/avatar.h
./main/stackchan/avatar/avatar/elements/emotion.h
./main/apps/app_avatar
./main/apps/app_avatar/app_avatar.h
./main/apps/app_avatar/app_avatar.cpp
ぞろぞろ出てきましたね。
main/stackchan/avatar/
main/stackchan/avatar/avatar.h
main/stackchan/avatar/avatar/avatar.h
main/stackchan/avatar/elements/emotion.h
main/apps/app_avatar/app_avatar.cpp
main/hal/hal_ws_avatar.cpp
この辺りが怪しそうです。
find main/stackchan/avatar -maxdepth 3 -type f | sortもうちょっと絞り込みます。
satoshi@Satoshi-MacBook-Pro firmware % find main/stackchan/avatar -maxdepth 3 -type f | sort
main/stackchan/avatar/avatar.h
main/stackchan/avatar/avatar/avatar.h
main/stackchan/avatar/avatar/decorator.h
main/stackchan/avatar/avatar/elements/element.h
main/stackchan/avatar/avatar/elements/emotion.h
main/stackchan/avatar/avatar/elements/feature.h
main/stackchan/avatar/avatar/elements/key_elements.h
main/stackchan/avatar/avatar/elements/speech_bubble.h
main/stackchan/avatar/decorators/angry.cpp
main/stackchan/avatar/decorators/assets/decorator_angry.c
main/stackchan/avatar/decorators/assets/decorator_dizzy.c
main/stackchan/avatar/decorators/assets/decorator_heart.c
main/stackchan/avatar/decorators/assets/decorator_shy.c
main/stackchan/avatar/decorators/assets/decorator_sweat.c
main/stackchan/avatar/decorators/decorators.h
main/stackchan/avatar/decorators/dizzy.cpp
main/stackchan/avatar/decorators/heart.cpp
main/stackchan/avatar/decorators/shy.cpp
main/stackchan/avatar/decorators/sweat.cpp
main/stackchan/avatar/skins/default/default.cpp
main/stackchan/avatar/skins/default/default.h
main/stackchan/avatar/skins/default/eyes.cpp
main/stackchan/avatar/skins/default/mouth.cpp
main/stackchan/avatar/skins/default/speech_bubble.cpp
これで構造がわかってきました。
main/stackchan/avatar/
│
├── skins/
│ └── default/
│ ├── eyes.cpp ← 👀 目はここ
│ ├── mouth.cpp ← 👄 口はここ
│ ├── default.cpp
│ └── speech_bubble.cpp
│
└── decorators/
├── heart.cpp
├── angry.cpp
├── shy.cpp
├── sweat.cpp
└── dizzy.cpp
C++かー(トラウマ)
ソースを確認すると以下の内容がわかりました。
LVGLの図形オブジェクトで顔を描いているみたいです。
目の正体
_eye = std::make_unique<Container>(_container->get());
_eye->setRadius(LV_RADIUS_CIRCLE);
_eye->setBgColor(primaryColor);Container という四角形を作る
角を LV_RADIUS_CIRCLE にして円にする
背景色を付ける
まぶたの正体
_eyelid = std::make_unique<Container>(_container->get());
_eyelid->setBgColor(secondaryColor);目の上に、背景色と同じ図形を重ねて隠すことで、
まばたき
細目
笑顔
眠そうな目
を表現しているみたいですね。やるなー
表情の仕組み
case Emotion::Happy:
apply_style(72, 1550);この2つの値で表情を変えています。
weight:まぶたで目をどれくらい隠すか
rotation:目全体の傾き
たとえば笑顔では、目を少し細くして大きく回転させています。
んー、これってオリジナルの顔にできるのだろうか。
というかやってる人がたくさんいるのでできるのであろう、
やるっきゃない!
スタックチャンの顔を画像で切り替えする方法
このままLVGL描画ではオリジナルの顔にはできないので他の方法を探してみようと思います。
ナルエビちゃん のスタックチャン表示用に表情を作った pic.twitter.com/1NeWgSp1Bx
— Null-O (@GOROman) May 10, 2026
goromanさんがこんな感じで、いろんな表情を作ってナルエビちゃんを作成されています。可愛いですよね。
どうせやるならオリジナルのイラストを用いてやって見たい。
そこで、こちらのライブラリを参考に画像を切り替えるやり方を考えて見ます。
こちらは
ArduinoやLovyanGFXを前提に作られているみたいなのでそのままは使えないっぽい(やりようはあるかも。)
ライブラリをそのまま組み込むのではなく、「画像切り替えの考え方」のみ参考にします。
今のスタックチャンならそこまで難しく考えなくてもいいかも。
いくつか画像を用意して見ました。
すべて生成AIで作ったものです。


こんな感じで感情にあわせてバリエーションを上位しました。
ひとまずは、
ESP32起動
↓
neutral.png表示
ってところまでを目指します。
StackChan/firmware/
├─ assets/
│ └─ avatar/
│ ├─ angry.png
│ ├─ blushing.png
│ ├─ crying.png
│ ├─ happy.png
│ ├─ neutral.png
│ ├─ sleepy.png
│ ├─ smile.png
│ ├─ sparkling.png
│ ├─ surprised.png
│ └─ thinking.png
│
└─ main/
└─ stackchan/
└─ avatar/
└─ skins/
└─ image/
├─ image.cpp
└─ image.hfirmwareの配下に以下のようにファイルをおきました。
image.cppとimage.hはまだ空です。
これらをビルドすることで、画像を呼び出す作戦です。
CMakeLists.txtを確認すると、
file(GLOB_RECURSE STACK_CHAN_SOURCES
...
"stackchan/*.cpp"
)
となっているので、多分全部ビルド対象になると思われます(たぶん、絶対だ)
StackChanの顔をオリジナルPNG画像に変更する方法
~既存の顔パーツを使わないImageAvatarの実装~
はじめに
StackChanは通常、目・口・眉などのパーツを組み合わせて表情を作っています。
そのため、イラストレーターが描いたキャラクターやオリジナルキャラクターをそのまま表示することはできません。
今回はStackChanの仕組みを変更し、
PNG画像をそのまま顔として表示するImageAvatar
を実装しました。
従来のStackChanの仕組み
通常のStackChanは
LeftEye
RightEye
Mouth
という3つのFeatureを生成し、
各Modifierが
まばたき
呼吸
口パク
表情変化
を制御しています。
Avatar
├── LeftEye
├── RightEye
└── Mouthこれらを組み合わせることで様々な表情を実現しています。
ImageAvatarという新しいスキンを作成
今回は
main/stackchan/avatar/skins/imageという新しいスキンを追加しました。
構成は
image/
├── image.cpp
└── image.hです。
PNG画像を読み込む
画像はAssetsから取得します。
_current_image = assets::get_image("neutral.png");取得した画像をLVGLのImage Widgetへ設定します。
lv_image_set_src(...)これだけでPNG画像がそのまま表示されます。
Featureは一切作らない
通常のDefaultAvatarでは
LeftEye
RightEye
Mouthを生成します。
しかしImageAvatarでは
Featureを一切生成しません。
つまり
ImageAvatar
└── PNG画像という非常にシンプルな構造になります。
最初に発生した問題
画像は表示できましたが、
起動直後に
Guru Meditation Error
LoadProhibitedでクラッシュしました。
原因は
avatar.leftEye()
avatar.rightEye()
avatar.mouth()がnullptrだったためです。
原因
StackChanは
Modifier
が
leftEye()
rightEye()
mouth()を直接操作しています。
例えば
BlinkModifierでは
leftEye().setWeight()が実行されます。
しかしImageAvatarには
LeftEye
RightEye
Mouthが存在しません。
そのためnullptrアクセスになっていました。
hasMovableFeatures()を追加
ImageAvatarへ
virtual bool hasMovableFeatures() const
{
return false;
}を追加しました。
通常のAvatarでは
trueを返します。
これで
「このAvatarは顔パーツを持つか」
を判定できるようになりました。
Modifier側を修正
Modifierへ
virtual bool requiresMovableFeatures() const
{
return false;
}を追加しました。
目や口を操作するModifierでは
bool requiresMovableFeatures() const override
{
return true;
}としました。
修正したModifierは
BreathModifier
BlinkModifier
IdleExpressionModifier
ImuModifier
SpeakingModifier
です。
StackChanでは
requiresMovableFeatures()を見て
ImageAvatarでは自動的にスキップするようにしました。
Display側も修正
Modifierだけではありませんでした。
画面制御側でも
avatar.mouth()が直接呼ばれていました。
例えば
LISTENING
STANDBY
SLEEPなどの状態遷移時です。
ここも
if (avatar.hasMovableFeatures())
{
avatar.mouth().setWeight(0);
}とすることでクラッシュを防止しました。
完成

修正後は
PNG画像表示
Wi-Fi接続
AI.AGENT接続
音声認識
日本語会話
音声再生
speaking → listening復帰
まで正常に動作するようになりました。
今後の課題
現在は
まばたき
口パク
呼吸
は無効になっています。
代わりに
neutral.png
happy.png
thinking.png
listening.png
speaking.pngなど複数の画像を用意し、
状態に応じて画像を切り替える方式へ発展させる予定です。
これにより、
従来の「目や口を変形するStackChan」とは異なる、
イラストをそのまま活かせる画像アバター型StackChan
を実現できます。
今日はここまで、次は表情を復活させれるといいなぁ。
【参考にしていただけましたら、よかったらフォローと、スキ❤️を、そしてX(Twitter)でシェアと感想を頂けると幸いです】
いいなと思ったら応援しよう!
お読みいただきありがとうございました。もし参考になったり面白かったと思っていただけましたらサポートよろしくお願いします。次の執筆のモチベになります。