こちらは、エンジニア集会 Advent Calendar 2024の17日目の記事です。
この記事ではエンジニア集会のハッカソンで開発していた、VRChat用のQRコードジェネレーターについて紹介します。
エンジニア集会のハッカソン
2024年の8月から9月にかけて、VRChat上でエンジニア集会とPNGミュージアムがコラボしてハッカソンを開催しました。ハッカソンのテーマは「展」。「展」とVRに関するものであれば何でもOKなハッカソンでした。
VRChat上のQRコードジェネレーターを作ってみた
このハッカソンでは、VRChat上で任意のURLをQRとして表示するQRコードジェネレーターを作ってみました。ハッカソン期間中はQRコードが途中まで表示するところまでしか作れなかったのですが、せっかくなのでハッカソン後も開発を進め、システムを完成させました。
QRコードジェネレーターの仕様
「どうやって動いてるの?」と思われる方もいると思いますが、まずは仕様について説明させてください。
このQRコードジェネレーターは実は制限がかなり多く、現段階では実用的ではありません。具体的には下記の仕様で動いています
- 表示出来るQRコードはバージョン1のみです
- 441マスまでしか表現出来ません
- URLは17文字のものまでしか扱えません
- QRコードが全部表示されるまでに、黒マスの数 x 0.4秒 の時間が必要です(おおよそ2分くらい)
- 後からワールドに入ったユーザには同期されません
・・・こうリストしていくと、本当に使えないなこのQRコード\(^o^)/
どうやって動いてるの?
アニメーションとOSCの組み合わせで動いています。
アニメーションについて
アバターに白い板を仕込み、441個(21マス x 21マス)の黒いマスが全て個別のアニメーションで白い板を突き抜けるように設定しています。そしてこれらの個別のアニメーションをOSCを使って操作をしています。
具体的には、1番左上の黒マスを操作するのに下記のpython用のOSCの操作をし、
client.send_message("/avatar/parameters/QRCode", 1)
2番目の黒マスを操作するのに下記のようにpythonを書いています。
client.send_message("/avatar/parameters/QRCode", 2)
上記の例の場合、QRCode
を1
に設定した後に2
に設定したら、1番目の黒マスは元の位置に戻るんじゃないか?という疑問があると思います。通常の設定ならば確かに戻るのですが、アニメーションのWrite Defaultsをオフにしてる場合、上記の場合1マス目は元に戻りません。
この特性を使い、1から252番目のマスはQRCode
というパラメータで、253から441番目のマスはQRCode2
というパラメータで表示/非表示の操作を行っています。ちなみに1つのパラメータではMax256個の値しか扱えないので、パラメータを2つ使用しています。
操作するマスを決める方法について
文字列をQRコードに変換する仕様は決められており、キーエンス社のウェブサイトから確認することが出来ます。
でもこのプログラムでは、その仕様はガン無視する方向で進めています。
まず、pyqrcodeというpythonのライブラリを使い、任意のURLをQRコードに変換します。
qr = pyqrcode.create(url, version=1, error='L', mode='binary')
その後、そのQRコードを0と1のマトリックスで表現をしています。
def print_qr_grid(qr_matrix):
# Print the QR code as a grid of 0s and 1s
for row in qr_matrix:
print(''.join(['1' if cell else '0' for cell in row]))
def main():
print_qr_grid(qr.code)
左上から順番に数字を読み込み、0だったら無視して、1だったらそのX番目の黒マスのアニメーションをOSC経由で動かしています。
qr_string = ''.join([''.join(['1' if cell else '0' for cell in row]) for row in qr.code])
for i, char in enumerate(qr_string, start=1):
if char == '1':
if i <= 252:
# 1 から 252番目のマスを操作
client.send_message("/avatar/parameters/QRCode", i)
else:
# 253 から 441番目のマスを操作
client.send_message("/avatar/parameters/QRCode2", i - 252)
URLの工夫
17文字のURLしか使えない仕様ですが、その先頭が https://
の8文字で使われるため、実質残り9文字しか使えません。この残りの文字数は無駄に出来ません。https://www
で始まるURLはwww
を抜いても問題ないので、プログラムに渡すURLから自動的に削除するようにしています。
if url.startswith("https://www."):
url = url.replace("https://www.", "https://", 1)
elif url.startswith("www."):
url = url.replace("www.", "", 1)
また、URLが/
で終わるURLは/
を取り除いても問題ないので、この処理も実装してます。
if url.endswith("/"):
url = url[:-1]
表示スピードの工夫
表示スピードが遅いですね!というのもこれには理由があります。
まず、OSCでのパラメータ操作をVRChatに一気に送ると、VRChat側が処理しきれなく、反応しません。
0.1秒ほどの間を開けてOSCでのパラメータ操作をVRChatに送ると反応はしてくれるのですが、大量のリクエストを送るとアバターが30秒ほどペナルティをくらい、OSC操作を受け付けない状態になってしまいます。
色々と検証した結果、0.4秒ほどの間隔で送ることにより、OSC操作を連続で送ることができ、かつペナルティを受けなくなります。
qr_string = ''.join([''.join(['1' if cell else '0' for cell in row]) for row in qr.code])
for i, char in enumerate(qr_string, start=1):
if char == '1':
if i <= 252:
# 1 から 252番目のマスを操作
client.send_message("/avatar/parameters/QRCode", i)
else:
# 253 から 441番目のマスを操作
client.send_message("/avatar/parameters/QRCode2", i - 252)
time.sleep(0.4)
開発できました!
制限が色々とあるとはいえ、https://booth.pm/
、https://x.com/
やhttps://yahoo.com
といった別々のURLをQRコードとして生成することが出来ました。また、きちんとカメラで読み込むことが出来たので、一応QRコードの生成には成功しました٩( 'ω' )و
とはいえ、この制限をなるべく緩くするために今後改善していきたいと思います。QRコードのバージョンを上げつつ、QRコード生成の仕様を理解した上で開発すれば、違った形で実装出来るのではないかと思います。
今後もQRコードジェネレータの開発を頑張っていきたいと思います!