JavaScript
Unity
VRChat

VRChat, My World, Your Word

2018/9/3追記: WebPanelは2018年8月に無効化されました。いまのところ復活の目処は立っていません。

概要

 VRChatのワールドを自作・公開したので経緯をまとめる。

筆者について

 UnityとBlenderをさわりはじめて1か月ちょいの初心者。VRChat自体は去年からやっていたものの、カスタムアバターやワールド作成に手を出しはじめたのはつい最近。

最初の一歩(アバター編)

 最初は本当に何もわからなかったので、VRCModsで適当にアバターをダウンロードして、チュートリアル動画の通りにやったら、それだけで普通にカスタムアバターを使えるようになった。
 ここで配布されているアバターはすべてVRChat用に最適化されており、ポリゴン削減等の面倒な工程を経ることなくアップロード可能なので、最初の一歩としておすすめである。

 そのあと、Blenderで作った簡単なオブジェクトをアバターに付けてみた。

アバター

 ちなみにこのしょうもないステータスウィンドウを付けるだけで5時間くらいかかった。なんだこの難しさは……。Blenderやべぇよ……。
 しかしこんなしょうもないカスタムでもVRChat内で「おもしろいね」と言ってくれる人が多く、なるほどこんなに即座に反応をもらえるのならアバター作成にハマる人が多いのもうなずける、と思った。

最初の一歩(ワールド編)

 でも僕にとってのVRChatはやはりコミュニケーションツールであり、これ以上のカスタムは不要に思えた。このステータスウィンドウも海外の人たちとのコミュニケーションを円滑にするために付けたものでしかないし。
 他人の作ったすごいアバターやパーティクル芸やシェーダー芸やらを見るのは好きだけど、それを自分で作ろうとは思わなかった。だからUnityやBlenderをさわるのはもういいかなと思っていた。

 きっかけはたぶんとてもささいな会話で、あまりよく覚えていない。最近になってワールド作成をはじめたフレンドが何人かいた。その人たちがとても楽しそうにワールド作成について話してくれた。そんなに楽しいのなら自分もちょっとやってみようかなと考えた。きっかけはそんなささいなことだったと思う。

 最初はねこますさんのチュートリアル動画の通りにやってみた。10分でワールド作成→アップロード→VRChatにログインして自分のワールドに入ってみる、というところまでいけた。マジか。こんな簡単なのか。ねこますさんサンクス。

Unityことはじめ

 ワールド作成の基本はわかったけど、Unityの基礎からやったほうが最終的に近道な気がしたので、まずはドットインストールのUnity入門を全部やった。後半はスクリプトの話になるので、カスタムスクリプトが許可されていないVRChatにはあまり関係なくなってくるけど、シーン、マテリアル、プレハブといったUnityの基本的な概念はここですべて学べた。
 とにかくゲームオブジェクトにコンポーネントをどんどん追加する。そのことによってゲームオブジェクトに様々な属性や機能を持たせられる。そのゲームオブジェクトを組み合わせてなんかいろいろやる……という感じっぽい。Unityを起動すると、ヒエラルキーとかインスペクタとかウィンドウがいっぱいあって最初は面食らったけど、それも前述の作業をやりやすくするためのものにすぎない、ということがわかった。

プロトタイピング

 それじゃあVRChatのSDKにはどんなコンポーネントがあるんだろうと思って公式ドキュメントをパラパラと読んでいくと、VRC_WebPanelという項目が目に入った。どうもこれはVRChat内でブラウザを使うためのコンポーネントらしい。ウェブ系言語なら自分の得意分野だし、これを利用して何かできないだろうかと思った。
 そしてプロトタイプとして作ってみたのが以下のバーチャルキーボードである。

 Unityを無視してブラウザのみを用いた動作から解説する。

Chrome画面

 まずはブラウザのアドレスバーに about:blank と打ち込んで、空白ページを開く。そのあとさらに javascript:document.write('A') と打ち込む。ちなみにコピペすると javascript: の部分が自動的に削ぎ落とされるので注意。
 このあとエンターキーを押すと、画面上にAと表示されるはずである。続けて javascript:document.write('B') とするとさらにBが表示される。ウェブ開発をやっている人にはおなじみ、JavaScript疑似プロトコルを用いた1行プログラムの実行である。これをVRChat上で実現する。

Unity画面

 まずはQuadオブジェクトを新規作成して、Add ComponentからVRC_WebPanelを追加し、上の画像のようにStart URIを about:blank にする。これで最初に空白ページが開かれるようになる。

Unity画面

 次にCubeオブジェクトを新規作成して、Add ComponentからVRC_Triggerを追加し、上の画像のように設定する。さきほどと違い、javascript:location.hash='#'+Math.random();document.write('A'); とURIをランダム値で書き換えているのは、同じURIが続くとページ遷移が行われず、プログラムが実行されないから(→同じ文字を連続入力できない)。
 これでローカルテストしてみると、Cubeに対してInteract(マウスクリック等)したときにWebPanelに文字が書き込まれる。

VRChatローカルテスト画面

 ローカル環境では関係ないからさっきは説明を省略したけど、マルチプレイヤー環境で重要なのは、WebPanelのSynchronize URIにチェックを入れることと、TriggerをAlwaysUnbufferedにしておくことだ。こうすることで同じインスタンスにいるクライアントすべてのWebPanelのURIが同期される。URIが同期されるということは、この場合、同じJavaScriptプログラムが実行されることを意味する。つまり、同じインスタンスにいるクライアントすべてのWebPanelに同じ文字が書き込まれる、ということになる。

URI同期概念図

 もしかしたらこれでテキストチャットを作れるんじゃないか? と思った。けっこう長いことVRChatをやってきて、その間いろんなワールドに行ったけど、バーチャルキーボードを用いたチャットなんてものは、どこにもなかったはずだ。けっこう需要はあるのになんでまだ誰も作ってないんだろうと思ってたけど、ないなら自分で作ってしまえばいい。

 この仕組みの利点はいくつかある。
 普通にウェブ上のチャットをWebPanelに表示させると、すべてのインスタンスで同じページを開いてしまい、同じインスタンスのメンバーだけでチャットをするのが難しくなってしまう。もちろんチャットにルーム機能を持たせて対応するというのもありだと思うけど、ログイン作業が必要になって面倒だ。VRChatでは手軽なものが好まれる傾向がある。
 そして外部サーバが不要、というところも重要だ。同期処理に関してはVRChat内で閉じているため、外部にウェブサーバやDBを用意する必要がなく、開発側にとってもお手軽なのだ。
 さらにSDKの不具合を回避できる、というのも大きい。実はVRC_WebPanelのIs Interactiveにチェックを入れれば、テキストボックスにフォーカスを当てたとき、VRChatログイン時や検索時に使用するソフトウェアキーボードを使えるのだけど、このあたりはまだ不具合が多く、マルチプレイヤー環境でうまく動作しないことが多い。URIを同期させる部分に関しては不具合はないので、この方式であれば、既存の多くの不具合を回避できるのだ。

 よし、なんかいけそうな気がしてきた。この方向で進めてみよう。

HTML

 ここまでは1行プログラムでやってきたけど、もう少し本格的なものにするため、HTMLファイルを読み込む形にする。
 VRC_WebPanelにHTMLファイルを読み込む方法はいくつかあるようだ。

  • Start URIにインターネット上のURIを入れる
  • ローカルにHTTPサーバを立てて127.0.0.1でアクセスする(当然ながらローカルテスト時のみ有効)
  • Path To Web ContentからUnityプロジェクト内に置いたHTMLファイルをパス指定する

 このうち、開発時に便利なのは3番目である。ただ、いくつか問題点がある。

  • JavaScriptを別ファイルにして <script> タグから読み込むとローカルテスト時は動作するが、アップロード時にビルドエラーになる → JavaScriptをHTML内に書けばOK
  • ローカルのHTMLを指定したWebPanelを含んだプレハブから複数個のゲームオブジェクトを作成した場合、たまに1つだけ正常に動作しないことがある(しかもワールドに入り直せばいけたり、発生条件・頻度は不明・不定期) → HTMLファイルをプロジェクトに組み込むのではなく、インターネット上から読み込むようにすればOK

 このような感じなので、開発中はローカルパスを指定する形にしてコード修正→確認のサイクルを速くし、あるていどフィックスしたらGitHubにアップ→GitHub Pagesで静的ファイル配信→WebPanelのStart URIでそのURIを指定する、という形に落ち着いた。

JavaScript

 Unity上でブラウザの動作を確認しなければならない状況というのはそう多くはなく、開発の9割以上はブラウザだけで完結する。
 まずは以下のようなHTMLファイルを作成して、Chromeで開いてみる。

<!DOCTYPE html>
<html>
<head>
<title>VRC Text Chat</title>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
$(function() {
  var input = $('#input');

  window.key = function(key) {
    location.hash = '#' + Math.random();
    input.val(input.val() + key);
  };
});
</script>
</head>
<body>
<input id="input" type="text"/>
</body>
</html>

 key という関数が window 直下に定義されていて、これは外部から呼び出せる。つまりさきほどの javascript:document.write('A') と同様、javascript:key('A') とアドレスバーに打ち込めば、<input> に文字が追加されていく。
 でも毎回アドレスバーに打ち込むのも面倒なので、開発中はDevToolsのコンソールから打ち込むのが楽でいいと思う。ChromeのDevToolsはF12で起動できる。

Chrome画面

 このコンソールに key('A') と打ち込めば、アドレスバーからの実行と同様の結果が得られる。ちなみに↑↓キーで打ち込んだコマンドの履歴が辿れるので活用して欲しい。
 HTMLファイルを編集し、Chromeをリロードし、DevToolsのコンソールで実行して確認する、というサイクルで開発を進める。

 さらに少し改良してみよう。

<!DOCTYPE html>
<html>
<head>
<title>VRC Text Chat</title>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
$(function() {
  var input = $('#input');
  var logs = $('#logs');

  var keys = {
    BS: function() {
      input.val(input.val().slice(0, -1));
    },
    ENT: function() {
      var log = $('<div>').text(input.val());
      logs.append(log);
      input.val('');
    }
  };

  window.key = function(key) {
    location.hash = '#' + Math.random();
    if (keys[key]) {
      keys[key]();
    } else {
      input.val(input.val() + key);
    }
  };
});
</script>
</head>
<body>
<input id="input" type="text"/>
<div id="logs"></div>
</body>
</html>

 バックスペースとエンターキーを実装してみた。

Chrome画面

 ちゃんと動作しているようだ。
 このHTMLファイルをUnityのプロジェクトに持っていく。

Unity画面

 さらにさきほどのCubeのトリガーを javascript:key('A') のように変更しておき、それを複製して、javascript:key('B')、バックスペース用の javascript:key('BS')、エンターキー用の javascript:key('ENT') も作っておく。

Unity画面

 実行してみる。

VRChatローカルテスト画面

 なんとなくチャットっぽくなっている。

コライダー

 やはりせっかくのVRなのだし、直接バーチャルキーボードをタイピングしたい。でもデフォルト状態ではプレイヤーの手にコライダーは入ってない(VRC_Player Modsのhealthを追加すれば手にコライダーが入るけど、細かい作業には向かない)ので、タイピング用のPickupオブジェクトを用意する。

Unity画面

 適当なオブジェクトにVRC_Pickupコンポーネントを追加して、コライダーのIs Triggerにチェックを入れる。
 キーボード側にはOnEnterTriggerを追加しておく。

Unity画面

 動いた。

キーボード入力

 デスクトップユーザー用に、リアルキーボード入力にも対応する。
 まずはキーボード入力ハンドラーを作成する。デフォルトでアクティブにしてしまうと常に全員が入力可能な状態になってしまうので、親の空オブジェクトを非アクティブにしておく。

Unity画面

 そして特定の1人だけが入力できるような仕組みにするため、Stationを作成する。
 新規作成したCubeオブジェクトにVRC_Stationコンポーネントを追加し、Disable Station Exitにチェックを入れる(WキーでStationから抜けてしまうのを防ぐため)。Station Enter/Exit Player LocationはTransormだけを持った適当な空オブジェクトを指定しておく。

Unity画面

 同オブジェクトにさらにVRC_Triggerコンポーネントを追加し、OnInteractがLocalでのみ実行されるようにする(Advanced Modeにチェックを入れることで選択可能になる)。SetGameObjectActiveでHandlersとExitButtonをアクティブに、アクション追加時に出てくるメニューをEvents From Scene -> Enter Button -> UseStationと選択していき、SendRPCアクションを設定する。

Unity画面

Unity画面

 ExitButtonはデフォルトで非アクティブにしておき、さっきと逆の設定をしておく。位置はEnterButtonと同じ場所で、Scaleを少しだけ大きくしておくことで、EnterButtonがExitButtonの中に隠れる形になり、EnterButtonがアクティブになっていても、ExitButtonをInteractできる。

Unity画面

 これでEnterButtonを押した人にとっては、

  • Stationに入る
  • ExitButtonがアクティブになる
  • EnterButtonが見えなくなる
  • キーボード入力が可能になる

 という状況になり、他の人にとっては、

  • Stationが使用中になる
  • ExitButtonは非アクティブのまま
  • EnterButtonは見えるがInteractできない
  • キーボード入力はできない

 という状況になる。
 これでマルチプレイヤー環境でのキーボード入力を実現できた。

コンセプト

 さて、ここまでで自分がやりたいことの技術的な検証は終わった。あとはゴリゴリ実装していくだけだ。しかしその前に、ワールド全体のコンセプトを考えなければならない。
 僕はグラフィック方面のスキルが皆無なので、コンセプトを考えたとしてもそれをグラフィカルに表現することはできないのだけど、どのようなプロダクトにせよ、基本コンセプトというのは細部に宿るものである。

 僕は何がしたいんだろう、と考える。僕はこのワールドを作って、どうしたいんだろうか。いやそもそも、僕にとってVRChatとは、なんなのだろうか。
 少し前に増田で書いたけど、VRChatをやっていて思い起こすのは、黎明期のインターネットだ。あやしいわーるど、テレホーダイ時代の2ちゃんねる、ベータ版のニコニコ動画。僕の中でVRChatはその流れの中に組み込まれている。
 思えば、インターネットなんて一部のマニアだけがやっている得体のしれないものでしかなかったのに、いつのまにこんなことになったんだろう。全世界が光の速さで情報交換できるようになったことでさえ驚きなのに、いまではVRによって感覚すら空間を超える。
 インターネットは確かに世界を変えた。VRもまた、世界を変えるだろうか。

 そんなことを考えていると、とある動画を思い出した。Google Chromeのオフィシャル動画、初音ミクが題材のやつだ。

Google Chrome : Hatsune Miku
Google Chrome : Hatsune Miku

 僕はこの動画が好きだった。初音ミクというインターネット史上最大級のミームをこれ以上ないほどにうまく表現していて、世界の広がりを感じさせてくれる。
 そうだ、ワールドの名前は、この曲のタイトル "Tell Your World" をもじって、"Tell Your Word" としよう。このワールドのメインはキーボード、そしてそれによるテキストチャットなんだから。
 (無言勢でも会話できるから)君の言葉を教えて、という意味を込めて。

UIデザイン

 コンセプトが決まったら次はUIデザインだ。このワールドのメインはキーボードによるテキストチャット。だから文字をいかに見やすくするかに注力する。
 最初はキーボードの前に大きなチャット画面を置いていたけど、この配置だとみんなが同じ方向を向いてしまい、VRの意味が薄れる。対面でお互いの挙動なんかも見ながら話せるのがVRの醍醐味だ。だからできれば向かい合って話せるようにしたい。でも、どうやって?
 そこで考えたのが、文字の切り抜きだった。VRC_WebPanelの設定項目をよく見ると、Transparent Backgroundというオプションがあり、これを有効にすることで背景を透過できるようだった。これを利用して、文字だけが浮かび上がるようにする。

<!DOCTYPE html>
<html>
<head>
<title>TEST</title>
<style>
body {
  background: transparent;
  color: white;
  font-size: 40px;
  text-shadow:
     1px  1px black,
     1px -1px black,
    -1px  1px black,
    -1px -1px black;
}
</style>
</head>
<body>
TEST
</body>
</html>

 このHTMLファイルをTransparent Backgroundにチェックを入れたWebPanelにセットしてみると、以下のようにちゃんと文字が切り抜かれた。

VRChatローカルテスト画面

 ちなみに今回は白文字なので黒で縁取りをしている。このように違う系統の色で縁取りをしておくと、背景が同系色で埋もれそうになっても視認性が保たれる。ちょっとした気遣いだけど、こういうのはわりと重要だ。
 文字が浮かび上がりやすいようにワールド全体は暗め、寒色系に。すべてのオブジェクトは半透明に。Duplicate Screensとマスクシェーダーを利用して、自分の手元は入力フォーム+ログ、対面から見るとログだけが表示されるように。リアルタイム性重視で、最新の発言だけを大きく、古いログは小さく。
 よしよし、いい感じになってきた。

オーディオ、アニメーション、ライティング

 キーボード入力中であることを近くの人に知らせるため打鍵音を付ける。音量調節は意外と難しくて、Audio SourceのPlay On AwakeとLoopにチェックを付けて音が鳴りっぱなしの状態にしておいて、近づいたり離れたりして何度も確認しつつ最適値を求めていった。
 jQueryのアニメーション機能を利用して入力テキストは下からスライドしてくる感じに。Unityのほうもキーをタイプしたときに色を付けたり、沈下させたりして打鍵感を上げる。さらにAnimatorを使って小さいアバター用のエレベータも作った。
 ライティングはベイクしないとクライアントに負荷がかかる、という話を聞いたのでさっそくやってみたら、オブジェクトによって暗くなったりならなかったりして意味がわからなかったけど、このページの通りにやったら一瞬で解決した。

本気出す

 本気出した。

完成

 完成した。

ベータテスト

 超高速タイピングの人が何人かいたけど、問題なく動作していた。

VRChat画面

Public化

 作成したワールドを一般公開するにはVRChat運営への申請が必要である。Discordの #public-world-request に詳細が書かれているので、それに従って申請し、しばらく待てばPublicになる。審査状況はこのスプレッドシートで確認できる。
 僕の場合、申請してから半日くらいでPublicになっていた。

VRChat画面

世界を変えるもの

 何気ない会話をきっかけとしてはじめたワールド作成だけど、なんとかPublic化まで持ってこれた。その間、Unityのおもしろさを知ることができたし、いろんな人とのつながりもできた。

 最近、少し自分は変わったんじゃないかと思う。
 これまでは社会とのつながりを極小にして生きてきた。SNSなんてかろうじて持っているFacebookアカウントで年数回の生存報告をする程度だったのに、先日、生まれてはじめてTwitterアカウントを作ったし、いくつかのDiscordコミュニティにも参加した。以前の自分からは考えられないことだ。

 ああそうか、と思う。
 VRChatで出会う人たちとの何気ない会話や、たわいないやり取りが、こんなにも愛しく思えてしかたないのは、きっとそれが、僕の世界を変えていくから。
 きっとそれこそが、「世界を変えるもの」だから。

VRChat画面