本記事は、横浜市並びに横浜サイエンスフロンティア高等学校に非公式で公開しているものです。
ただし、記事に含まれる動画に関しては、公開許可を取得しています。
本記事に関する問い合わせを市・高校へ行う行為は慎んでいただくようお願いいたします。
また、著作権は著作者に帰属します。
本記事内には、多少容量のあるGIFアニメーションが含まれています。
モバイルデータ通信の方は通信容量にご注意ください。
0. 概要
何したん?
2023年3月 高校生活最後の学年集会で、プロジェクションマッピング && 学年集会からプロジェクションマッピングへいい感じにつなぐ用のアプリを作りました。
↓ぜひ、こちらをご覧ください! (前半は 編集ミスの関係で音と映像がズレてしまっています...🙏)
制作したソフトウェアはこちらからもアクセスできます
1. はじめに
こんにちは。毎日欠かさずTwitterでのアウトプットを心がけている ただのツイ廃やん! もぐもぐと申します。
タイトルの通り高校生最後の学年集会で開発・プロジェクションマッピング制作を行ったので、振り返りつつ知見を伝えられたらと思います。
whoami
$ who -b
system boot 2004-09-17
$ whois @YumNumm
👨 NAME : Ryotaro Onoue
🐤 TWITTER : @YumNumm
🏫 WORK : YSFH 12th -> YUMEMI Inc. Flutter Enginner // 入社エントリ書き途中…
💕 LOVE : Flutter
👀 INTERESTED IN : Kotlin/Go
3. 何をしたの?
主に2つの制作活動を行いました。
4. Flutterでリアルタイム制御アプリケーション制作 と 5. プロジェクションマッピングの制作 分けて話していこうと思います。
4. Flutterでリアルタイム制御アプリケーション制作
ここでいう、「リアルタイム制御ソフトウェア」というのは、キーボード入力に応じて状態が変化するのみであり、センサー等を使っていないことを先に申し上げておきます。
- 本章内の、コードは実際のコードから記事の説明に不要な部分を省いています。
実際のコードを確認したい方は以下のリンクよりご確認ください。 - とりあえず動くことを第一優先に開発を行いました。パフォーマンスや保守性の面で 相当雑なコードとなってしまっています。
この制作に関して、横浜市立横浜サイエンスフロンティア高等学校の12期生が 『どんな学年だったのか』 について説明する必要があります。
4-1. YSF12thはどんな学年だったのか?
YSF12thは、横浜市立横浜サイエンスフロンティア高等学校附属中学校(YSFJH)の1期生徒と高校からのメンバーが混在する初の学年で、融合の年次と呼ばれていました。
高校附属中学校のメンバーと高校から入ってきたメンバーが完全に混ざり合い、共に同じスタートラインから学び始めるのは、神奈川県内の高校において、非常に珍しいケースです。
- YSF12thは高校初の融合の年次であること
- 元素番号が12のマグネシウムを表すこと
などから、学年主任は入学当初から "Miracle Grade" (奇跡の年次)というスローガンを掲げていました。
毎回の学年集会の学年主任のパートでは、Miracle Gradle
の頭文字M
とG
で始まるメッセージを頂いてきました。
そこで今回は、このMiracle Grade
をうまく活用する方向で話を進めることにしました
4-2. 制作陣の紹介
- ソフトウェア開発については、全て自分が担当しました。
- 動作チェックや意見交換は 後述するプロジェクションマッピング制作メンバーも関わっています
4-3. Rukasoからの注文
このプロジェクトの企画者であるRukasoからの注文です。
あくまでも学年集会という体で始まっているため、サプライズ要素としてプロジェクションマッピングにうまく繋げる必要がありました。
様々な演出を検討した結果、 「先生方がジャンプしすぎて壊れちゃった」 という演出にすることになりました。
-
1,2,3,4,5,6,7,8,9,0,-(Q),~(W)
の各キーを押した時に、割り当てた文字が出現する -
B
キーを押して、画面が壊れていくエフェクトを表示する
(デバッグ用要件)
-
スペースキー
を押して、状態を初期化する(リセット) -
Enter
キーを押して、すべての文字を出現させる - 画面タップでも文字を光らせられるようにしたい
上記の要件を満たし、1ヶ月以内の納期で完成させるために Flutterを用いて開発を行いました。
フレームワーク | Flutter 3.7.3 (開発当時最新バージョン) |
状態管理 | Riverpod v2.1.3 (with riverpod_generator) |
Flutterで開発した副次的効果として、macOS以外のデバイスでも動作させることができました。
本来、この効果はあまり期待していませんでしたが、試しにビルドしてみたら思ったよりもうまく行ってしまい、Flutterの便利さを再確認することができました。
また、これにより、macでうまくプロジェクターに投影できなかった際のフォールバックとしてWindows PCを用意するなどの対応ができました。
4-4. 仕様を考える
実際の成果物はこんな感じに動作します。
(最後の方に、Bボタンを押して壊れるアニメーションが開始しているのですが、どこかで実装をミスったか なにかで描画が遅くなっていますね... 時間あるときにリファクタリングしてみようと思います
ブロックノイズ部分の再描画回数を減らす実装を入れた気がするので そこでバグっていそう この重い感じも逆になんか良い演出のように思えてきた)
4-4. 実際の実装について
4-4-1. キーボード入力を検知する
キーボードの入力に応じて表示を変えたいため、当然キーボードの入力を監視する必要があります。
Flutterでキーボードの入力を監視するために、FocusableActionDetector
を用います。
まずは、Intent class
を作成し、
class OnKeyPressIntent extends Intent {
const OnKeyPressIntent(this.target);
final int target;
}
// ...
Map<Type, CallbackAction>
型を返す関数を作成し、Intent
発火時の挙動を設定します。
Map<Type, CallbackAction> getActions(WidgetRef ref) {
return {
onInvoke: (OnKeyPressIntent intent) {
ref.read(actViewStateProvider.notifier).onPress(intent.target - 1);
return null;
}
// ...
}
次に、Map<SchortcutActivator, Intent>
型の定数で、Intentを発火させるトリガーを設定します。
const shortcuts = <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.digit1): OnKeyPressIntent(1),
SingleActivator(LogicalKeyboardKey.digit2): OnKeyPressIntent(2),
SingleActivator(LogicalKeyboardKey.digit3): OnKeyPressIntent(3),
SingleActivator(LogicalKeyboardKey.digit4): OnKeyPressIntent(4),
SingleActivator(LogicalKeyboardKey.digit5): OnKeyPressIntent(5),
SingleActivator(LogicalKeyboardKey.digit6): OnKeyPressIntent(6),
SingleActivator(LogicalKeyboardKey.digit7): OnKeyPressIntent(7),
SingleActivator(LogicalKeyboardKey.digit8): OnKeyPressIntent(8),
SingleActivator(LogicalKeyboardKey.digit9): OnKeyPressIntent(9),
SingleActivator(LogicalKeyboardKey.digit0): OnKeyPressIntent(10),
SingleActivator(LogicalKeyboardKey.minus): OnKeyPressIntent(11),
SingleActivator(LogicalKeyboardKey.keyQ): OnKeyPressIntent(11),
SingleActivator(LogicalKeyboardKey.keyW): OnKeyPressIntent(12),
SingleActivator(LogicalKeyboardKey.caret): OnKeyPressIntent(12),
SingleActivator(LogicalKeyboardKey.enter): ResetIntent(),
SingleActivator(LogicalKeyboardKey.space): ShowAllIntent(),
SingleActivator(LogicalKeyboardKey.keyB): BreakScreenIntent(),
SingleActivator(LogicalKeyboardKey.keyL): SwitchShadowLevelIntent(),
SingleActivator(LogicalKeyboardKey.escape): ToMainViewIntent(),
};
あとは、FocusableActionDetector
を用いて、実際のWidgetを囲ってあげます
// Widget側
return FocusableActionDetector(
shortcuts: shortcuts,
actions: getActions(ref),
autofocus: true,
focusNode: useFocusNode(),
child: ...,
);
ショートカット周りに関しては Widget of the weekでも述べられていましたので、こちらもぜひご覧ください。
FocusableActionDetector |
Shortcuts |
---|---|
4-4-2. 効果音を再生する
キーボードからの入力を受け取って効果音を再生します。
この時に、同時にキーボード入力を受け取った際にも 各効果音が重なって再生される必要があります。
今回は、README.md
を見た感じ使いやすそうな、just_audio
プラグインを利用しました。
class ActViewState extends _$ActViewState{
@override
List<TextItem> build() => ...;
@override
void onPress(int target, {bool shouldSound = true}) {
if (shouldSound) {
// 音を鳴らす
final player = AudioPlayer(); // <-ここでインスタンス化する
final fileName = 'sounds/${(target + 1).toString().padLeft(2, "0")}.mp3';
player.setAsset('assets/$fileName').then(
(_) => player.play(),
);
}
...
}
AudioPlayer
クラスをonPress()
関数内でインスタンス化して、再生処理を行っているのがキモですね。
各オーディオファイルに対して(=各キー) AudioPlayer
クラスをインスタンス化したものを保持する方法も試してみましたが、やはり同じキーが高頻度で押された際に、再生が中断され 再度再生される という挙動になってしまいました。
この方法には一つ課題があって、再生のたびにassets/*.mp3
をAssetsからファイルを読み出しているため、コンマレベルではありますが若干再生が遅れるという問題があります。
Dart側でオーディオファイルをキャッシュする実装を入れるべきでした。
ちなみに、Flutter on the webだとAssetファイルを要求した時点で、普通にFetchが走るのと同時に Service Worker側でもファイル要求が走って、後者は以降のリクエスト用にキャッシュされるみたいですね!
優秀!
4-4-3. 文字を輝かせる
各文字に対して以下の3つの状態を持たせます。
以上の3つの状態に応じて表示を切り替えていく必要があります。
輝いている状態について
文字が輝いているように見せるように、2つのWidgetを重ねて描画をします。
- 背景のグラデーションっぽいやつ(最背面)
普通のText Widget
を子に持つ、ImageFiltered Widget
を使っています。
ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: 60,
sigmaY: 60,
),
child: Text(
textItem.text,
style: style.copyWith(
color: textItem.color,
),
),
),
- 文字の本体について
至って普通に、Text Widget
を用いてTextを描画します。
Text(
textItem.text,
style: style.copyWith(
color: textItem.isHead
? const Color.fromARGB(255, 255, 0, 0)
: Colors.black,
),
)
ImageFiltered Widget
は様々な箇所で活用可能ですね! とても便利です!
4-4-4. 壊れていくアニメーションの実装
最初の頃は、古き良き時代の アナログテレビのノイズ画面を想像していました。
しかし、Rukasoと検討を重ねた結果、現在のような ブロックノイズ風の演出になりました。
この部分の実装については、CustomPainter Widget
とAnimation
を利用して実現しています。
(今見返すと結構問題山積みのコードを書いています。再描画回数が多くなっていますし、必要ないのに グローバル変数で定義しています。 当時納期ギリギリでなかなかキツイ精神状態だったことお許しください!)
AnimationController? noiseAnimationController;
class _NoiseWidgetState extends ConsumerState<NoiseWidget>
with TickerProviderStateMixin {
/// ノイズの強さ
double noiseLevel = 0;
// ...
@override
void initState() {
noiseAnimationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 10),
)..addListener(() {
// ...
// noiseLevelをいい感じに更新する処理
setState(() {
// noise部分の更新頻度調整 = 基本60Hz/10 = 6Hz
if (_counter % 10 == 0) {
noiseLevel = noiseAnimationController!.value;
}
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
// ...
return CustomPaint(
painter: _NoisePainter(
Size(constraints.maxWidth, constraints.maxHeight),
noiseLevel,
),
);
}
(これまたツッコミどころの多いコードになっています.. maxSize
は引数で渡す必要はありません...size
が使えます...
Random()を毎回インスタンス化するのはよくなさそう...)
class _NoisePainter extends CustomPainter {
_NoisePainter(this.maxSize, this.noiseLevel);
/// ノイズレベル 0.0 ~ 1.0
final double noiseLevel;
final Size maxSize;
@override
void paint(Canvas canvas, Size size) {
final random = Random();
// 閾値チェック
for (var i = 0; i < pow(noiseLevel, 3) * 40; i++) {
final x = random.nextInt(maxSize.width.toInt());
final y = random.nextInt(maxSize.height.toInt());
final paint = Paint()
..color = const Color(0xFF000000)
..style = PaintingStyle.fill;
canvas.drawRect(
Rect.fromLTWH(
x.toDouble(),
y.toDouble(),
(random.nextInt(maxSize.width ~/ 10) + 10).toDouble(),
(random.nextInt(maxSize.height ~/ 10) + 20).toDouble(),
),
paint,
);
}
}
@override
bool shouldRepaint(covariant _NoisePainter oldDelegate) {
return oldDelegate.maxSize != maxSize ||
oldDelegate.noiseLevel != noiseLevel;
}
}
5. プロジェクションマッピングの制作
テーマ: 高校生活3年間の振り返り、将来羽ばたいていくサイエンス生の姿を映し出す。
映像では、写真からポリゴンを描き、ポリゴンでできた人や物をAfterEffects内で動かすという手法を用いました。
これにより、特定の個人が写りすぎてしまう・外部に公開できなくなってしまう問題を解消することができました。
制作期間 | 約1ヶ月 |
投影場所 | YSFH アリーナ(体育館) |
利用プロジェクター | Maxell MP-WX5503J |
制作人数 | 4人 |
制作物の長さ | 9分42秒 |
利用ソフトウェア | - After Effects - Illustrator - Blender - AviUtl - OBS |
映像内の流れ
全体の流れ
プロジェクションマッピング開始までの流れ
-
学年の先生方から 最後の一言をいただく
-
最後の学年主任が、「年次集会といえば この言葉ですよね!」と確認
-
ただ、肝心の
Miracle Grade
の部分が表示されない(謎に先生方が学年主任の元へ集まってくる) -
1人先生が飛び出して、ジャンプをするとそれに合わせて、画面上に文字が一文字ずつ効果音と共に出てくる。
-
先生が12人全員で終わり、みんな跳ねていると 画面が乱れ始める
-
Miracle Gradeの効果音が勝手に音を奏で始める
-
めちゃくちゃかっこいい3D映像
プロジェクションマッピング映像内の流れ
-
2023.3.2
: 上映当日
過去
YSFで過ごした3年間を振り返る
-
2023.03.02 -> 2020.03.01
: 過去に戻る -
2020.04.07
: 入学式 正門 -
2020.04.07
: 入学式 廊下 -
2020.04.08->06.01
: 休校期間 暗い廊下で誰もいない -
2020.09.16
: 文化祭 各クラス企画の廊下 -
2021.11.21
: 沖縄研修 飛行機離陸 -
2021.11.21
: 沖縄研修 1日目に宿泊したホテル -
2021.11.22
: 沖縄研修 美ら海水族館 -
2021.11.22
: 沖縄研修 現地での有志発表 -
2021.11.23
: 沖縄研修 飛行機着陸 -
2022.05.17
: 3年次の体育祭 -
2022.09.10->09.11
: 3年次文化祭 各クラス企画の廊下
現在(2023.03.02
)
- 12thを表す新たなロゴの出現
未来
YSF12thが様々な場所で活躍する姿を表現しています。
高校生の間つけていた、みんな共通の 校章という目に見える形のものが無くなったとしても、
2023.03.02->203x.xx.xx
- Teacher(先生)
- Researcher(研究者)
- Architect(建築家)
- Doctor(医者)
- Pharmacist(薬剤師)
- Announcer(アナウンサー)
- Developer(開発者)
- Performer(パフォーマー)
- Pilot(パイロット)
- 同窓会シーン
エンドクレジット
学年主任・担任・学年集会オープニングムービー・クイズ企画・プロジェクションマッピング制作 に関わった人の目元と名前が順番に表示されていきます。
5-1. 制作陣の紹介(本企画に関わった制作陣全員)
Rukaso
発案、構成、映像(4:10~11:30)、作画、ロゴデザインを担当
Twitter: https://twitter.com/Rukaso__04
いさ
楽曲、映像(2:23~4:10)、ロゴデザインを担当
Twitter: https://twitter.com/illwav
もぐもぐ
システム開発・操作(0:19~2:15)、作画を担当
Twitter: https://twitter.com/YumNumm
むしゃむしゃ
構成、映像(11:30~12:08)、作画を担当
Twitter: https://twitter.com/k_648648
5-2. 制作の流れ
上記4人で制作を行いました。
制作開始自体は、12月上旬頃から画面等の調整は開始しましたが、チームメンバーの受験関係等でしばらく作業は行わず、2月頭から納期1ヶ月弱の制作が始まりました。
当初は、プロジェクターを体育館の中央に置き、ステージ部分に投影する予定でしたが、途中から体育館の一番うしろまで下げ、前面をフルで活用することにしました。
本来、このプロジェクターはアリーナのステージに出てくるスクリーンにスライド等を映し出すためのモノです。
しかし、カーテンや出入り口を閉じて、アリーナ内を十分に暗くすれば、映像を見る分には問題ないくらいの輝度を確保できました。
事前に方眼を投影し、この座標を元にプロジェクションマッピング投影映像用に位置合わせをしていきます。
Before | After |
---|---|
このデータや、今まで撮ってきたクラスメイトとの思い出の写真、校内の写真を用いて映像を制作していきました。
また、せっかくアリーナの全面にどデカく映像を映し出せるのであれば〜! と、とてもカッコいい映像をいさが制作してくれました。
ちなみに、実際に投影した映像 や 制作の裏側の小話 は ディレクターズカット版で公開予定です。
(が、あいにく制作者が全員忙しくて思うように進んでいないのが現状です かなしい)
5-3. 映像伝達のお話
映像・音声伝達の面で一つ問題が発生していました。
今回は、「あまりバレずにいきなりプロジェクションマッピングを開始したい」という背景があり、プロジェクションマッピングの切り替えにあまり時間的・場所的なコストを書けることができません。
また、学年集会自体のオープニングや他団体の演出の関係で映像・音声を今まで通り普通に行う必要がありました。
そこで、プロジェクションマッピング用の大きめなプロジェクターとは別に、ステージ近傍に普段授業で利用しているプロジェクターを配置し、プロジェクションマッピング開始直前にプロジェクターを切り替えるという運用を行いました。
ここで問題になってくるのが、音声と映像の伝達方法です。
特に、プロジェクションマッピング周りに関しては、音声を放送設備のあるステージ横に持っていく必要があるのに対し、映像をアリーナ最後尾のプロジェクターまで持っていく必要があります。
この問題を解決するためにいくつか解決策を考えました。
- ネットワークを介して配信する
- 校内LAN既存の配信システムを利用する (<- よく固まるで有名)
- Discordなどの外部のサーバを経由して配信を行う (<- 当日生徒が入ることを考えると帯域幅やIP割当で問題が発生する可能性がある)
- 無線LANを独自に作成し、LAN内で配信を行う (<- 面倒だし構築・動作検証時間がない)
- 有線で直接持っていく
- HDMIで直接接続する (<- そんなに長いHDMIケーブル無いし、用意するのに時間がかかる)
- HDMI on LANを利用する (<- Rukasoがちょうど持っていた。神!)
ということで、学校にあったとても長いLANケーブルとRukasoが持っていたHDMIをLAN経由で伝達できるアダプターを利用して映像伝達を行うことにしました。
数回伝達テストをした結果、十分実用だということがわかったので この運用で行くことに決定しました。
5-4. シールも作ったよ
この発表に関して、下のようなロゴを作成し 生徒/年次の先生方に配布しました。
ロゴ作成の背景は色々あります。
「サイエンスフロンティアの12期生・先生方が解散し、離れ離れになっても このシールという共通のものを持っている。またいつかこの校舎に集まろう」 という思いがありました。
5-5. 当日レンダリング失敗していたお話
発表前日の深夜までイラスト作成や調整を行っており、最後完成したのが 当日の朝1時ごろでした。
フルでレンダリング・出力を行うと 外部GPUを用いても4,5時間ほどかかる関係で、寝る前に処理開始して 翌朝起きたらその出力結果を用いて当日発表する流れでした。
でした。
そうです。問題が起こったのです。
当日レンダリング終了しているはずの処理が失敗していたのです...
結局、前日までに部分部分で出力していたものを当日AviUtlでつなぎ合わせ それを用いました。
部分部分でバックアップ用に出力してくれていなかったら、発表することはできませんでした。チームメンバーに感謝です。
結果、いくつか不具合が残っており 完璧な状態で発表することができませんでした。
6. まとめ
このように横浜サイエンスフロンティア高等学校では様々な活動を行うことができました。
3年生の文化祭を始め、この最後の学年集会の制作活動でも様々なことを学ぶことができました。
神奈川県、いや、全国を見てもこのような活動ができる高校はなかなかないのではないでしょうか。
優れた仲間と一緒に優れた製作活動を行うことができたのは、同学年の仲間たちの力、またそれを許可してくださる
先生や方々の協力のおかげだと思います。
7. この制作に関わって の自分の感想
Flutter楽しい!
読者の皆さんがどのくらいFlutterについて詳しいのかわかりませんが、Flutterは本当に楽しいです。
AndroidのMaterial UIに関連するComponentが豊富なのに加え、Dartの型安全・Null安全で効率的にロジックを記述できる点も推しです。
何より、Hot Reload/Hot Restartでコンマ秒単位でコードでの変更を適用できる点は今回の開発でも非常に便利でした。
ぜひ皆さん一度はFlutter触ってみてください!
7-1. Adobeが思った以上に面白かった
私は今まで開発を行ってきたので、映像制作などの手法はあまり知りませんでした。
今回の製作活動を通じて、Adobe Illustratorを用いて素材作りを行いました。
また、チームメイトが制作していく様子を間近に体感し、今まであまりいい印象がなかった Adobeが、とても面白く創造的なものだと感じました。(この悪い印象の原因は、単に Twitter でクラッシュしているなど情報を見るからだと思います。)
7-2. そうだ、高校最後なんだ。
この制作を行い、自分たちが作ったものを実際に学年の前で披露した時、自分の足はガクガクでした。
もちろんちゃんと動いてくれるかなどの心配はありましたが、どちらかというと「あ、高校生活もう終わってしまうんだな」という気持ちもありました。
高校生活はやり直しことができません。人生で1回しかない。高校生活が終わってしまうんだと思うと、とても悲しく思いました。
高校を卒業した自分は、「もう一度高校生に戻ってやり直したい.... もっと....」と思う時が多々あります。それは、二度とやり直せないからこそ感じることができるように思えます。
高校生に還元したい
自分の高校での経験や体験を、現在の高校生に伝えていくような活動をしていければと思っています。
母校である 横浜サイエンスフロンティア高校では、SL(Science Literacy)という授業があります。
このSLの高校1年生向けの授業プログラム SL1 の「情報のサイエンス」という授業のTA(Teaching Assistant)に参加し、高1と意見を交わしたりする機会がありました。こちらも様々なことを学ぶことができ とても良い回だったと思います。
(TAに参加した後に 近くの鶴見川を眺めながら 高校時代を思い出していましたw)
他にも、進路フォーラムやサタデーサイエンスなどの様々な行事・授業で母校に貢献する方法はあります。
自分の体力と相談しつつ 貢献していければと思っています
7-3. やっぱ、創るのってたのしいんだわ。
ここまで記事を読んでくださった方はもしかしたらご存知かもしれませんが、私は以前文化祭のクラス企画でもソフトウェアの開発を行いました。
様々な人と協力しながら時に揉め時に励まし合い作り上げていく。その感覚はとても楽しく、貴重なものだったと今では思います。
卒業してからわかりましたが、なかなかこういう機会はないのです。損得関係なく、様々な制作活動を行うことができるのは学校生活ならではのものだと思います。
もしこの記事を読んでいる高校生、中学生の方がいれば是非その貴重な時間を無駄にせず、自分のやりたいことに全力を尽くすべきだと私は考えています。
7-4. これから何する?
私はまた、今回のような制作活動を行いたいと思います。
この制作が終わり3ヶ月が経ちましたが、まだあの制作をしていた日々がつい昨日のように感じられます。
またみんなで何かを高い熱量で創り上げていく そんな経験をしたいものです。
(次は同窓会で何かやりたいよな〜 という話も出ていますが ハードルが高くなかなか厳しいように思えます。)
8. おまけ: 準備の様子
写真を何枚かおいておきます。
↓ 作業員変身しつつ 開発をする自分(もぐもぐ)
↓最後の限界まで振り絞る制作陣のサポート物資