見出し画像

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描画ではオリジナルの顔にはできないので他の方法を探してみようと思います。

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.h

firmwareの配下に以下のようにファイルをおきました。
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)でシェアと感想を頂けると幸いです】

いいなと思ったら応援しよう!

ドイのnote お読みいただきありがとうございました。もし参考になったり面白かったと思っていただけましたらサポートよろしくお願いします。次の執筆のモチベになります。