製作目的
息子のリハビリ・歩行器&車椅子練習のモチベーション(好奇心)を引き出したい!
が一番のきっかけでした。
そして、その好奇心を引き出す手段として「子供が好きなビジュアルコンテンツを組み合わせてみたい」と新しくチャレンジしてみようと思いました。
今年1月に東京旅行で遊びに行った「アクアパーク品川」
水槽のある部屋一面に投影されたプロジェックション空間。
壁を触るとフワフワとパーティクルが浮き上がり、興奮する息子!
気が付けば、1時間以上もその場を離れようとしない息子
今まで息子の好奇心を引き出すために、おもちゃを改造したり、LEDで光らせたり、音を鳴らしたり、いろいろと試行錯誤してきましたが、この様なビジュアルコンテンツはどの手段以上に子供の好奇心を引き付ける魅力がある事を痛感しました!
これを、例えば普段の生活の中や、普段のリハビリ、歩行器練習/車椅子練習に取り入れていくと、
もっと息子のやる気・モチベーションを引き出す事ができ、
「できる」の可能性を広げる事ができるのではないだろうか!?
というワクワク心がきっかけです。
しかし、私自身は本業でも趣味でもハードウェア(特に電気ハード寄り)専門なモノづくりをする事が多く、ビジュアルコンテンツはもちろんソフトアプリの開発経験がありませんでした。
そこで、まずは ゲームコンテンツの作り方を学ぶべく、Unityを勉強しはじめました!
2020年2月からスタート、とりあえずUnityの参考書を写経してC#やUnityの構造の基礎を学びました。
何を作ろうかなぁ、と考えていたところでの、
突然のコロナ禍
約2か月近くのStay Home、外来リハビリを受けれないという課題に対面した事を通して、
・外出できない子供達におうち生活を刺激的に楽しませたい!
・外来リハビリに通えない息子と、立位などのおうちリハビリを楽しく進めたい!
(息子が自分から主体的に体を伸ばす様に誘導したい)
を目指したバーチャル&アトラクションリハビリシステムを作ってみようと考えました!
VRヘッドセットをつければ、比較的簡単に没入感の高いビジュアルコンテンツを楽しむ事はできるかもしれません。
しかし残念ながら、息子は年齢的にもVRゴーグルを装着できず、また体に装着する事を極端に嫌うため、VRを体験する事はできません。
VRゴーグルが使えないならば、息子の周りの空間をVRっぽくすればよい!
という発想転換で、
自宅内に
チームラボみたいな
プロジェクションゲーム壁(インタラクティブ壁)
を開発してみました!
制作物①
壁をタッチするとボールを転がせるプロジェクションゲーム壁 (2020/5)
外出できない子供達を楽しませたい & リハビリに通えない息子に楽しく体を動かして貰おうと、
— おぎ-モトキ@父親エンジニア (@ogimotoki) 2020年4月30日
自宅にチームラボみたいなプロジェクションゲーム壁を作った!
壁をタッチするとボールが飛び出し、転がる!
ボール好きな息子は興奮!
最上段から転がそうと体を伸ばしてる♪#家族のためのモノづくり pic.twitter.com/rcCUp2FfTo
息子が大好きな「ボール転がし」を寝室の壁上にバーチャル再現しました。壁をタッチするとそこからボールが現れ、そのまま坂道を転がっていきます。壁の跳ね返りでタイミングがあれば、ボールは激しく飛ばされます。最終的に右下にあるマトに当たればゴールです!
狙い通り、息子は興奮して、食い入る様にボールの転がる様子を見続ける!
興奮して、自然と足で踏ん張り、簡単な支え付きで立位を取ってくれます!
そして、最上段からボールを転がしたい欲が激しく強く、何度も手をおもいっきり上げてくれる!
よし、狙い通り!!!
制作物②
壁をタッチすると色を塗りたくれるプロジェクションゲーム壁 (2020/6)
壁を触るとインクで塗りまくれるスプラトゥーン風プロジェクションゲーム壁を自宅に作った!
— おぎ-モトキ@父親エンジニア (@ogimotoki) 2020年6月6日
顕微鏡風デバイスで近くの色を捕まえて、それを壁に塗りつける
子供はお絵かき大好き!
不自由ながら楽しそうに全身を動かし一生懸命に塗っていく息子
楽しくおうちリハビリだ!#家族のためのモノづくり pic.twitter.com/9c2VUBydTZ
スプラトゥーン好きな娘や妻にも遊んで貰える事を目指し、壁を手でインクべたべたに塗りつぶせるゲームを作りました。顕微鏡風デバイスの上のLEDを押すと、その下にある色を捕まえれる! その色を使って、壁に塗る色を自分で選びます!
色の認識を身に着けるという学習効果も狙っています!
参考図書
Unityの最初の学習として、この2冊を読みました!
①Unityの教科書 Unity2019完全対応版 2D&3Dスマートフォンゲーム入門講座
Unityの概要やゲーム制作のプログラム構成などを理解するのがやりやすかったです。(最初の一歩向き)
②Unity 3D/2Dゲーム開発実践入門 Unity 2019対応版
①では触れ切れていなかった細かい設定や、本格的なゲーム制作を体験するのに良い一冊でした
また、Unityを学ぶにあたり、以下のサイトを参考にさせていただきました。
xr-hub.com
www.notion.so
システム構成
本プロジェクションゲームは、プロジェクタとセンサーを使っています。センサーはLiDARと呼ばれる測域センサーを使って壁面にある物体を検出しています。
このセンサーで物体=人の手足を検出した場合、その測定距離&角度に対応したゲーム画面位置にイベントを発生させる仕組みになっています。
こちらの構成詳細については、こちらのサイトに詳しく掲載されています。
ハードウェア
■プロジェクタ
家庭での使用を想定した場合、投影場所&距離が課題になります。我が家はマンションで広くないため、できる限り単距離から投影できる機器が望ましいです。また、自宅だけではなく、リハビリ病院や友人宅など複数環境で実施する際の携帯性を配慮した結果、「超単焦点タイプ」のプロジェクタを購入しました。
これを使えば、壁だけでなく、天井や床など多様なプロジェクションゲームを楽しめます。
■センサー
壁平面(二次元)上の物体を検出するのに、今回はLiDARと呼ばれる測域センサーを使いました。これは、車の自動運転やロボットの自律移動におけるSLAM/Navigationで定番として使われるセンサーです。
本命は、北陽電機のURGセンサーです。
0.25度単位の解像度で最大4mを検出できる精度が何よりの魅力です。Team Labの会場でも、北陽センサーのシリーズ品が壁にとりついているのを見ました(たぶん)
ただし、価格が高価で個人購入をするには抵抗があるため、今回は廉価LiDARの使いこなしの検討も含めて、RPLiDAR社のLiDARを使ってみました。
これは、物理的に回転する筐体タイプ、0.7~1度単位の解像度とつかいにくい面も多いです。ただ、価格が1/10という魅力があり、横幅2m程度のスクリーン幅なら1cm程度の検出精度は確保できそうなので、子供向けリハビリシステムとしてであれば使えそうですね。
物理的に回転するため縦置きしづらい点は3Dプリンタで固定治具を制作しました。
■ゲーム②固有: 色検出デバイス
スプラ風プロジェクションゲームの色捕獲デバイスは、#M5atom+色センサーを使用
— おぎ-モトキ@父親エンジニア (@ogimotoki) 2020年6月7日
光るボタンを押せば、新しい色を捕まえれる
画面表示よりも直感的&刺激的なLEDは息子好み!#Unity で製作したゲーム側とBluetoothSerialで接続して遊ぶ想定だが、これだけでも色を学ぶ学習キットとして使えそう! pic.twitter.com/5SFlPfEYKf
上記制作には、PCと無線接続可能かつ小型デバイスとして「M5Atom」を使ってみました。本体PCとの通信は省電力化も踏まえて「Bluetoothシリアル」としました。
部品名 | 数量 | 購入先 |
---|---|---|
①M5Atom Matrix | 1個 | Amazon |
②色検出センサ | 1個 | スイッチサイエンス |
③5V昇圧回路 | 1個 | Amazon |
④単三×2電池パック | 1個 |
制作システム
■Lidarから距離検出方法
北陽LidarのUnityライブラリは以下のサイトを参照ください。(C# サンプル などが参考になります)
URG センサとのやりとりには SCIP コマンドを使うそうで、SCIPライブラリも忘れずにインポートしておきます。
チュートリアルのページを参考に動作確認していきます。
また、RPLidarの導入に対して、まずは以下のマニュアルに従い動作確認を行いました。
http://bucket.download.slamtec.com/e680b4e2d99c4349c019553820904f28c7e6ec32/LM108_SLAMTEC_rplidarkit_usermaunal_A1M8_v1.0_en.pdf
確認としては、以下のframe_grabber を使用します。
Release release/v1.12.0 · Slamtec/rplidar_sdk · GitHub
また、Unityで使用する際には、以下のライブラリを使用します。
rplidar · GitHub Topics · GitHub
このサンプルを使って、RPLidarのクセを確認してみたところ、以下の点を注意する必要がありそうです。
・15cm以下は取得できない
・オブジェクトの境界線で0値になることが多い
・1周期内の取得データでも同じ角度における距離情報ではない(角度はデータ行列内のthetaに格納されている)
■人タッチ位置の検出方法
上記でセンサーから周囲物との距離一式が取得できる様になれば、
ゲームスタート時点でのセンサー情報との差分を見れば、人のタッチ位置と思われる座標を検出することができます。
そこでポイントになるのは、「どこまでが一つ物体と捉えるのかクラスタリングする」事です。
この辺りは、以下のサイトの情報を参考にさせていただきました
github.com
koki0702.hatenablog.com
littlewing.hatenablog.com
https://www.iplab.cs.tsukuba.ac.jp/paper/master/shige_master.pdf
上記は北陽のLidarベースで、クラスタリングを行っていますが、この考え方をRPLidarのデータ形式に変更してやりました。
なお、「センサからの距離情報」→「ゲーム画面上での座標」への変換について、以下のサイトではホモグラフィ変換行列を使って、画面角4点から正確な座標値を取得していますが、
今回の用途ではそこまでの正確性はいらないと考え、2点(センター位置と右上)とプロジェクタ台形補正機能で設定しています。
(真面目に精度が必要なコンテンツになった場合に改めて考えたいと思います)
■ゲーム①固有開発 : 使用アセット等
ベースアセットは、当初 勉強として活用していたUnity本の例題に挙げられたゲーム。その舞台をベースに、Unity画面上で斜面オブジェクトを生成しました。
ゲーム画面での座標から3D世界上の斜面にボールを発生させるのに、ScreenPointToRay関数を使用しています。
また、タッチ時のチャタリングによりボールが過剰に発生しすぎない様に、ボール生成条件の中に「隣接のボールオブジェクトとの距離がD1以上離れている場合」を追加しました。
BGMについては、以下のサイトのモノを使わせていただきました。
■ゲーム②固有開発 : 使用アセット等
単なるペイントではなく、インクっぽいエフェクトで塗る感じを表現するため、InkPainter というアセットを活用しました。
InkPainterは設定されたテクスチャーをあたかもインクが塗られた様に上書きされていく様です。こちらの使い方は以下のサイトを参考にしました。
esprog.hatenablog.com
qiita.com
■ゲーム②固有開発 : M5Atomとの接続方法
USBによるシリアル通信、WiFiによるWebSocket通信など、いくつか手段は考えられますが、特にデバイス側の電池長持ちの観点や使いやすさの観点を踏まえて、「Bluetooth」を選択しました。そして、調べてみたところ、「Bluetooth Serial」であればPC側が通常シリアル通信と同じ手段で実施できるという事で、Bluetoothを使いたいと思います。
Unity側は、UniRxというプラグインを使ってSerialポートからデータをReadします。
using System.Collections; using System.Collections.Generic; using System.Threading; using System; using System.IO.Ports; using UnityEngine; using UniRx; using Es.InkPainter.Sample; public class SerialController : MonoBehaviour { public string portName="COM8"; public int baurate = 115200; private GameObject lrfPainter; public Vector4 color; SerialPort serial; bool isLoop = true; public bool isConnect = false; public bool useM5atom = true; public float timeOutDiscon = 5.0f; public float timeOutLoop = 20.0f; private float timeElapsed, timeDiscon = 0.0f; void Start() { lrfPainter = GameObject.Find("LrfPainter"); color = new Vector4(1.0f, 1.0f, 0.0f, 1.0f); this.serial = new SerialPort(portName, baurate, Parity.None, 8, StopBits.One); try { this.serial.Open(); isConnect = true; useM5atom = true; Scheduler.ThreadPool.Schedule(() => ReadData()).AddTo(this); } catch (Exception e) { isConnect = false; useM5atom = false; Debug.Log("can not open serial port"); } } private void Update(){ timeElapsed += Time.deltaTime; timeDiscon += Time.deltaTime; if (timeElapsed >= timeOutLoop){ if (useM5atom == true){ if (isConnect == false){ try { this.serial.Open(); isConnect = true; Scheduler.ThreadPool.Schedule(() => ReadData()).AddTo(this); } catch (Exception e) { isConnect = false; } } } timeElapsed = 0.0f; } } public void ReadData(){ while (this.isLoop){ string message = this.serial.ReadLine(); isConnect = true; if (message.StartsWith("RGB_")) { //Debug.Log(message.Substring(4, 2)); float r, g, b; r = (float)Convert.ToInt32(message.Substring(4, 2), 16)/255; g = (float)Convert.ToInt32(message.Substring(6, 2), 16)/255; b = (float)Convert.ToInt32(message.Substring(8, 2), 16)/255; color = new Vector4(r, g, b, 1.0f); timeDiscon = 0.0f; } } } void OnDestroy() { this.isLoop = false; this.serial.Close(); } }
M5Atom側は
#include <M5Atom.h> #include <Wire.h> #include "Adafruit_TCS34725.h" #include "BluetoothSerial.h" Adafruit_TCS34725 tcs = Adafruit_TCS34725(TCS34725_INTEGRATIONTIME_50MS, TCS34725_GAIN_4X); BluetoothSerial bts; float r, g, b; float ratio_h = 2.5; float ratio_l = 0.6; uint8_t DisBuff[2 + 5 * 5 * 3]; void setBuffP(uint8_t posData, uint8_t Rdata, uint8_t Gdata, uint8_t Bdata){ DisBuff[2 + posData * 3 + 0] = Rdata; DisBuff[2 + posData * 3 + 1] = Gdata; DisBuff[2 + posData * 3 + 2] = Bdata; } void setBuff(uint8_t Rdata, uint8_t Gdata, uint8_t Bdata){ for (uint8_t i = 0; i < 25; i++) setBuffP(i, Rdata, Gdata, Bdata); } void shftBuff(){ for (uint8_t i = 24; i > 0; i--) { for (uint8_t j = 0; j < 3; j++) DisBuff[2 + i * 3 + j] = DisBuff[2 + (i-1) * 3 + j]; } } void setup() { DisBuff[0] = 0x05; DisBuff[1] = 0x05; M5.begin(false, false, true); Wire.begin(26,32,10000); bts.begin("M5atom");//PC側で確認するときの名前 Serial.begin(115200); setBuff(0x20, 0x20, 0x20); M5.dis.displaybuff(DisBuff); if (tcs.begin()) { Serial.println("Found sensor"); } else { Serial.println("No TCS34725 found ... check your connections"); while (1); } tcs.setIntegrationTime(TCS34725_INTEGRATIONTIME_154MS); tcs.setGain(TCS34725_GAIN_4X); r = 255; g = 0; b = 0; } void loop() { uint32_t sum = 1; uint16_t clear, red, green, blue; M5.update(); if(M5.Btn.wasPressed()){ tcs.setInterrupt(false); delay(60); tcs.getRawData(&red, &green, &blue, &clear); tcs.setInterrupt(true); sum = clear; r = red; r /= sum; g = green; g /= sum; b = blue; b /= sum; r *= 256; g *= 256; b *= 256; if(r > 255) r= 255; if(g > 255) g= 255; if(b > 255) b= 255; } setBuff((uint8_t)r, (uint8_t)g, (uint8_t)b); Serial.print("RGB_"); Serial.print(int2str((int)r)); Serial.print(int2str((int)g)); Serial.println(int2str((int)b)); bts.print("RGB_"); bts.print(int2str((int)r)); bts.print(int2str((int)g)); bts.println(int2str((int)b)); uint16_t disp_color = (int)(r/8)*32*64+(int)(g/4)*32+(int)(b/8); M5.dis.displaybuff(DisBuff); delay(200); } String int2str(int n){ if(n < 16){ String ret = "0"; ret.concat(String(n, HEX)); return ret; }else{ return String(n, HEX); } }
終わりに
子供の好奇心を掻き立てさせ、
自宅にいながら体の動きを連動して部屋空間を作れるプロジェクション壁!
可能性の塊かもしれない!
ワクワク!!
そして、単なるバーチャル映像(ソフトウェア)だけでなく、色検出デバイスやおもちゃなどの物理的なデバイス(ハードウェア)を組み合わす事で、
子供の楽しみ方はもっと広がるんじゃないかなぁ、と実感してきた!
(いわゆる『バーチャルとリアルの掛け合わせ体験』ですな)
本当に継続して楽しめるか、そして体を動かす運動観点で意味があるのかはあくまで仮説。
色々と何パターンが作ってみて、息子や子供達にウケが良さそうなモノをどんどんと進化させていきたいなぁ、と思います。
センサーは床設置で持ち運びできる&簡単設定できる様にしてるので、もう少し改良したらいつものリハビリセンターや学校、友達の集まりに持っていきたいし、
あと、リハ先生には是非ともご意見&改善案など頂きたいところです