VRChatはじめました

※このエントリは「変わった/変わらなかったこと Advent Calendar 2020(https://adventar.org/calendars/5227」の24日目です

通称ぽっぽアドベントも今年で2回目!今年もまたアドベントに参加する形で、色んなひとと一緒に今年を振り返ることができて嬉しいです。

私は去年のアドベントでは、音楽フェスを観るため人生で初めて海外旅行をした話を書きました。

つまり去年は人生の中で一番家から遠くへ離れる経験をした年でしたが、反対に今年は人生の中で一番家から離れなかった年になりました。

海外フェスどころか、国内の音楽イベントにも全く行かなかった2020。

感染症の拡大や社会情勢とは別のところで、個人的に体調を崩し気味になってしまったこともあり、「もはや肉体、要らないなあ。肉体を捨て、ヴァーチャルな存在として生きていきたいなあ」、という願望は年末に向け高まるばかり(肉体があると寒いし)。

というわけで、VRChatをはじめてみることにしました。

VRChatとは

アメリカ合衆国の企業であるVRChat Inc.によって運営が行われているソーシャルVRプラットフォームである。

SteamまたはOculus Storeにて無料でダウンロードできる。Oculus Rift, HTC Vive等に対応したバーチャル・リアリティヘッドセットを使用してプレイすることが出来る。また、バーチャル・リアリティヘッドセットを利用しないデスクトップ版で遊ぶことも可能である。

wikipedia VRChatの項目より(https://ja.wikipedia.org/wiki/VRChat)

VRChatはボイスチャットでおしゃべりしたり、イベントなどで交流したりすることがメインなので、実際はあの映画みたいに壮大なバトルを繰り広げたり熾烈なお金稼ぎをしたりする必要はありません。

また、『レディプレイヤーワン』もそうですが、映画やドラマに出てくるVR世界というと「あらかじめ提供されたバーチャルなゲームの世界に入ってプレイヤーが遊ぶ」という印象が強いです。

しかしVRChatはむしろユーザー主導の場になっていて、アバターや、周りの環境(世界)そのものも自作できたり、各自がゲームを作ったりイベントを主催したり、遊び方そのものを好きに作っていくという側面があります。コミケとかボカロ文化とかとも親和性が高い気がして、日本のユーザーが多いのもうなずけます。

また、VRというと例の頭にかぶるゴーグルみたいなやつ(HMDというらしい)とか、専用のコントローラー等が必要なイメージですが、VRChatは普通のPCだけでも遊ぶことができます。わたしも特にVR機材は持っていないので、ゲーム販売/配信プラットフォームであるSteamからVRChatをPCにダウンロードさえすればプレイする準備は完了です。

Day1

この日はちょうど12月1日、わたしは自分の中だけで「VRChat初心者デビューアドベント」を開始しました。

VRChat(以下VRC)をはじめた者はツイッターで「#VRChat始めました」というタグをつぶやいてみると沢山反応がもらえる、という先人のアドバイスに従い、まずはその通りにしてみました。

詳細は後に回しますが、実はVRCを始めたばっかりの初心者はまだオリジナルのアバターを着て動き回ることができないので、まずはVRC側で提供されているアバターを使わせてもらうことになります。(かわいいのからイカツイのまで色々選べます)

わたしはなんだか『アイ, ロボット』とかに出てきそうな、いかにも人間の感情を理解することができなさそうな青いアンドロイド姿でVRCに突入することにしました。

Day2

ツイッターでアッピールもしたことですし、なにかリアクションもらえてたら嬉しいな、とわくわくしながらVRCにログインすると、なんとフレンド申請が来ている!!これでやっとVRCの入り口に立てた気がします。

後から知りましたが、この時にフレンド申請をくれたライテルさんという方は、VRCの色んな場所に現れては全く言葉を発することなく「そこにただ佇んでいる」ことでVRC界の妖精さんっぽい存在として都市伝説的に有名な方らしく、よく初心者にフレンド申請を送ってくれるようです。なんだかよくわからないですが、そこはかとなく優しさ的なものを感じます。

Day3

フレンドはできたものの、残念ながらすぐに誰かと気さくに交流できるようなスキルと人間性は自分にありません。

そもそもチャットに必要なヘッドセットマイク自体が家にないとか環境的ないろいろもあり、とりあえず会話せずになんとかならないか、と早くもじゃあVRCをやる意味ってなんですか的迷宮に迷い込みそうになりましたが、きっと自分以外にも「ボイスチャットはハードル高いよ」なひとはいるはずだと情報を検索したところ、「無言勢」というひとたちがいることを知ります。

”チャット”と銘打たれてはいるけれど、別にチャットしなくても友達は作れる。。これもひとつの多様性。。わたしはここにいていい!かもしれない!!

Day4

「無言でもいい」を自信にかえて、この日は勇気を出してVRCのソーシャルの舞台、「ワールド」に出向いてみました。

VRCには無人島だったりテーマパークだったり、日々増殖する様々な「ワールド」があって、自由に遊びに行くことができます。

ワールドでは、実際の街みたいに偶然そこに居合わせた人と自由に交流することもできれば、逆に制限をつけて自分が招いた人しか来られないようなプライベートな空間を作ることもできるのですが、わたしは詳しいことが分からなかったので、とりあえず万人に開けている適当なワールドにいきなり行ってみることにしました。

選択画面からピックアップしたワールドにワープしてみると、そこは夜のジャングルみたいな空間で、遠くの方の開けた広場で何人かが焚き火を囲んでおしゃべりしているようでした。

わたしは雰囲気に慣れるため、まずは木の陰に隠れて端っこのほうでただじっと彼らの様子を見ていました。

おそらく不審に思われたのでしょう。遠くにいたのでバレてないと思っていたんですが、焚き木の近くで談笑していたグループのひとり、猫耳をつけた美少女が突如猛スピードでこちらに近づいてきます。

その瞬間、考える暇もなくわたしは「ホームに戻る」ボタンを押してその場から消えていました。

あとから考えると木の陰からこっそり人を観察しておいてバレたら速攻で逃げるとか気持ち悪い人間すぎると思うし、相手は普通に交流を図ろうとしてくれていただけかもしれないのにと後悔してるんですが、理解できる言語で話しかけられるかもわからないし、そもそも話しかけられてもこちらは喋れないし。。

ヴァーチャルでもここまで交流にビビってしまうとは、まだまだ公共空間に入っていくのは自分には早かった、と反省の日なのでした。

Day5

そもそも「無言勢」、VR機器を持っていればジェスチャーである程度意思疎通できたりしますが(それこそ手話でコミュニケーションすることも可能なので、VRC内で手話講習会なども随時開かれているようです)、私のようなPCデスクトップのみでプレイしている人間が無言でやり通すのはなかなか難易度が高いのかも、と感じはじめます。

デスクトップ版にもジェスチャーや絵文字を表示する機能はあるのですが、これも慣れないと即座にその場にあった反応をするのは大変そうです。

その気概があれば観葉植物的にただそこにいるという参加の仕方もありとはいえ、小心者なのでノーリアクションを突き通すのもそれはそれでビビる。

なんとか実際に喋ることなくリアクションすることは出来ないか。。と考えていたところ、いわゆる「音声読み上げソフト」に喋ってもらう、という方法にたどり着きました。

棒読みちゃんという無料の読み上げソフトがあるようだったので、早速インストールしてみます。

テストしたところ、タイピングした文字をちゃんと喋ってくれました。いかにも機械っぽい音声ですが、これはこれで肉体を持たない者感を盛り上げてくれていいような気もします。

Day6

毎週土曜日に「Silent Club」というクラブイベントが開催されているらしいことを知ります。なにがサイレントかというと、そこに行くと全員強制的に音声にミュートがかかり、声が発せなくなるワールドになっているようです。

Silent Club公式サイト

これはまさにわたしが求めていた、非コミュ人向きのクラブイベントだ!!!!

誰もしゃべれないのだから、しゃべらないことを気に病む必要はありません。

それに、バーチャルでクラブイベントに参加できるなんて最高です。

オーガナイザーとフレンドにならないと入れないようだったので、とりあえずフレンド申請を出し、来週を待つことにします。

Day7

来週までにとりあえず他のワールドも回ってみようと思い、一人でも楽しめそうなワールドを探してみました。

AIとオセロ対戦が出来るワールドというのを見つけたので入ってみます。

なんとかオセロに勝つことができたので、ここで棒読みちゃんを導入したことを思い出して、「やったー」と話してみました。

しかし相手はAIなので、話しかけても反応はありません。

これでは自分がVRC内でちゃんと話せているのかわかりません。

こんなことでいいのだろうか。。

VRC内にあるペンは、空間に落書きすることもできます

Day8

この日もひとりで面白そうなワールドを探して遊んでみました。ファービーを投げるのは楽しいです。

でもいつまでもひとりで遊んでばかりいては棒読みちゃんの真価が発揮できません。

そこでゲームが得意なリア友に頼んで、ちゃんと自分がVRC内で音声を発することが出来ているか確認してもらうことにしました。

いろいろ設定を試した結果、最終的にVRC内で喋ることが可能に!

これでバラ色のVRCライフの始まりです!!

day9

しかし、これで実際に会話に反応するにはかなりのタイピング速度が必要。。

おまけに今わたしがメインで使っているキーボードは自作したもので、標準的なキーボードよりかなりキー数が少なくなっています。

(余談になりますが、自作キーボードはマツコの知らない世界でも特集されたりして今ひそかに(主に末広町あたりで)熱いのです!)

これでもひととおり必要なタイプはできるので、一応ちゃんと使えるものではあるのですが、いまだにちょいちょい打ち間違ったりキーを割り当てたポジションを忘れたりします。(でも見た目がかわいいから意地で使っています)

なのでこの日はよく使いそうなショートカットをまとめたり、「こんにちは」などの単語を素早くタイプする練習をしました。なにごとも練習が大事です。

TR-808を意識した配色の自キ

Day10

この日はメディアアートを展示している仮想の美術館がVRC内で公開されたとのお知らせをツイッターで見たので、行ってみることにしました。

美術館ならひとりで行ってもよさそうだし、万一誰かに話しかけられてもわたしには音声読み上げの術があります!かかってこいや!!

バーチャル美術館、本当に景色のいい美術館だったので興奮して動き回っていたところ。。

Day11&12

一瞬忘れてましたが、わたしは三半規管がげきよわなので3Dゲームとかやるとすぐ酔ってしまうのでした。無理をしてはいけなかった。。

盛大に酔ったことですっかりビビってしまい、土日はログインを控えることにしました。(土曜日はSilent Clubが開催されているというのに・・)

まあ「3D酔いは段々慣れる」という意見も見たので、くじけずにちょっとずつ慣れていきたいです。

とはいえ結構くじけてます。こんなことではいつまでたってもヴァーチャルに生きることなどできません。そもそも酔ってしまうのだって肉体があるのがいけない。誰もお前を愛さない。

Day13

さてこの日もログインできず、わたしは少し焦っていました。プレイしないと、その分「トラストレベル」が上がらないからです。

トラストレベルというのは何かというと、その名の通りユーザーの「信用」レベルなのですが、これはログインしてプレイした時間の多さとか、フレンドの多さとかを基に段階的に上がっていくものです。(明確な審査基準は分かっていないようですが)

要は荒らし目的とかじゃなくて普段からちゃんと遊んでる人間だよね、というのを証明するための仕組みですが、レベルが上がると何がいいのかというと、これでやっと自作したオリジナルのアバターをアップロードすることができるようになるのです!!(また、自作する以外にもboothなどで販売されている好きなアバターを使うこともできるようになります!)

やはり、VRCとほかのSNSとの大きな違いは、仮想肉体(アバター)を持ってヴァーチャル空間を歩き回れるという体験です。自分の好きな見た目になって世界を歩けたら楽しいですよね。

いつかわたしもそうなれたらいいなあ。。

Day14

まだまだトラストレベルは上がってくれないので(短時間しかログインできないせいですが。。)、今のうちにアバターを納得いくレベルに仕上げよう!と思い立ちます。

アバターにする用の3Dモデルは、blenderという無料(!!)の3Dソフトで作成することができます。

作り方はこの動画が初心者にもすごく分かりやすくて、もうこの通りにやれば誰でもいちからモデルを作れちゃいます。

(興味が出たひとは気軽にチャレンジしてみよう!)

Day15

説明動画を見つつ手直しを加え、VRC用の3Dモデルがなんとか納得できるレベルになってきました。トラストレベルが上がったら、こいつをVRCにアップロードして使ってみようと思います。

実はこれまでにも何回も作っては納得いかず作り直すを繰り返していて中々完成させられなかったんですが、一旦はこれで動けばオッケーということにしました。自分の能力に見合った妥協は大事ですね。

ちなみにこのキャラクターはなんかいつの間にか誕生していたアノンチャンというオリジナルのキャラです。いつの間にか誕生して以来、アイコンなどに使っています。一応このくすんだピンク色以外にも色んな色のアノンチャンがいるという設定があったりします。

Day16

なんかメールボックスにVRC公式さんからお知らせが届いていました。

開いてみると。。

自作アバターが使えるようになった通知!!

ちょうどモデルもいい感じに出来たところで、なんという神タイミング。ちょこちょこ隙間時間でログインしていたので、ついにトラストレベルがあがったんですね!

実際にモデルをアップロードするにはUnityというゲームエンジン(これも個人利用では無料)が必要だったり、見た目の調整などの細かい手順もあったりしますが、大枠は上に貼った動画で説明してくれているのでそれに沿ってアップロード作業をしていきます。

(アップデートでバージョンが変わっていたりでちょっとやり方変わっている部分もあったので、適宜ググっていきましょう)

Day17

しかし連日、日中はオフィスワークで夜はVRCでずっとPCに貼り付いているのも心身によくない気がします。たまには自然も見ないとね。

この日はちょっとお休みをとって動物園へ。

これはグンディという、ネズミに似たきなこもちです。もちなのに動いてます。かわいいですね。

2、3年前から動物園とか水族館に行くのがマイブームで、なにげに今年も隔月に1回ぐらいの頻度で行っていた気がします。

特に動物園は値段も安いし、敷地面積が広い園を狙えばそこまで密になることもないし、自然の中をめっちゃ歩くので健康にもいいしいいことずくめです。

ただこの日はマフラーを忘れて寒すぎたのでちょっと風邪ひいた気がします。

Day18

さて気分転換もできたところで、

勝利のアノンチャン

遂にアバターをアップロードできました!自分のキャラがVRC内で動いています!!感動~~。

上のツイート内の動画はあらかじめ登録されているジェスチャーをさせているのですが、VR機器を持っていれば実際の自分の動きに合わせてアバターを動かせるんだなあと思うと、やっぱりハードがほしくなりますね。。

ただ、ゴーグルを使ったときの画面酔いはデスクトップの比ではなさそうなのと、ぽんと買えるお値段ではないというところが厳しい。

今はレンタルサービスなどもあるそうなので、いつか1回試してみるのもありなのかなあと考えたりもしています。

なんにせよ、今はVRC内にアノンチャンを召喚できただけでも満足です。

顔がでかすぎて出来ないポーズが多いとか顔がでかすぎてパーツが埋もれるとか色々あるんですが、もう、かわいいのでOKです。ああかわいい。自分かわいい!!

Day19

「自分かわいい」という圧倒的自信を得たことによって、もう恐れることは何もなくなりました。

話しかけられてすぐ反応できなくても、音声の活舌がよくなくても、ジェスチャーが乏しくても、ただそこに突っ立っているだけで自分はかわいいからいいのです。

世のかわいいものたち(猫とか)ってこういう無敵な気持ちで生きているんでしょうか。。

そしてこの日はちょうど土曜日。Silent Clubが開催される日です。

ちゃんとオーガナイザーの人にフレンド申請は済ませたし、あとは時間になったら開催地に行くだけです。ついにバーチャルクラブイベントデビューだ!!!!!

その時のフロアの様子はこちら。

Silent Clubの様子

誰もいない…..?

おかしい。。狐につままれた気持ちでフロアを3周ぐらいしてみましたがやっぱり誰もいません。

イベント開催時間が21時からだったので、「じゃあ22時ぐらいに行けばいいのかな~」というタイム感覚で結局22時過ぎ頃行ったのが悪かったんでしょうか。。 ヴァーチャルのイベントは現実のイベントよりめちゃくちゃパンクチュアルで、1時間きっかりとかで終了して全員解散したあとだったのかもしれません。それか自分だけみんながいるのとは別次元のフロアに飛ばされてしまったのでしょうか。

謎はつきませんが、とにかくいないものはいないのです。ぼっちのフロアっていうのも現実ではあまり経験できないし、まあこれはこれでいいでしょう、わたしにはまだまだぼっちがお似合いなのです。

Day20

そんなわけで、結局まだ全然VRC上でまともなコミュニケーションをとるに至っていないわたしですが、これからもおそるおそる色んなワールドに行ってみたいと思っています。

当面の目標?はリアルで行けなかったクラブイベントや音楽イベントに行ってみることと、自撮りができるワールドで映え自撮りをとることです(映える姿になれたので・・・)。

もっと英語話者に話しかけて英会話の勉強がしたいとか、イベントで演奏する側にも回ってみたいとか志高いこと言いたいんですけど、エナジーが少なめの人間なのでとりあえずはそこらへんで。

そしてもしこの記事を読んでくれた人の中で「私も始めようかな」と思ってくれた人がいたら、ぜひフレンドになってください!

zoomとかmocriとかに比べると導入に手順が必要だったりPCのスペックもある程度要求されたりしますが、そのぶん臨場感はあるので、イベント後のオフ会とかで居酒屋ワールドに集まれたりしたら楽しいんじゃないかなーと思います。

あと、ワールドの中でyoutubeの動画とか表示して皆で見るみたいなこともできたと思うので、オタクプレゼンとかミーティングとかするのもいい気がします。

変わらなかったこと

以上がわたしの今年ちょっと変わったことでした。

今年は映画館に行く回数も減っちゃったり、その他生活形式の変化によって変わっちゃたルーティーンはいくつかありました。

しかしニューノーマルとか言われても人間性は簡単には変わりません。相変わらずオタクなのでツイッターではドクター・フーとかの話ばっかりしているし、相変わらず計画性がないのであれもこれもやりたいとおもいながら出来なかったことが沢山ありました。

まあでも年末までどうにか死ななかったので、それで十分かなあとおもっています。

みんなもできるだけ死なないようにして、できるだけ足は冷やさないようにして年末を過ごしてください。

さいごに

ぽっぽアドベント今年はみっつもあって毎日読み応えたっぷりですね!毎日全部読んでます。

野生のオーガナイザーはとさん、今年もお忙しい中アドベントの企画・運営ありがとうございます。はとさんのホスピタリティがみんなの「書きたい」気持ちに火をつけてるんだとおもいます。感謝!!

そんなアドベントも明日でなんとラストです!!!!

明日の担当は、みなみさんさん、そしてはとさんです。

three.jsでカスタム図形を作る(3):曲がるパイプを作る

ボーンの作成

ボーンに沿って曲がるようなチューブ状のメッシュを作成する。

ボーンとなるベジエ曲線を作成し、セクション数ごとに分割した地点の座標を取得する。

//start、end、controlポイントの座標作成
const center2D = new THREE.Vector2();  
const ep = new THREE.Vector2( 10,20 );
const cp = new THREE.Vector2( 10,0 );
//ベジエ曲線を作成
const curve = new THREE.QuadraticBezierCurve(center2D, cp, ep);
//セクション数に分割した座標の配列を取得
const sect = 4;
const bone = curve.getPoints(sect);
bone配列を作成

※sect数4でgetPointsするとbone配列のサイズは5になる。

分割したボーン間の角度を求める

let zpos = bone[0];
for(let i=0; i<(sect+1); i++){
    //zpos(bone[n-1])とbone[n]の角度を求める
    const diff = new THREE.Vector2().subVectors(bone[i], zpos);
    const angle = Math.atan2(diff.y, diff.x);
}

ボーンに沿った円の座標を取得

const edge = 12;
const size = 5;
const pt = [];
for(let i=0; i<(sect+1); i++){
    //上記angleを求める式(省略)
    pt[i] = [];
    for(let j=0; j<edge; j++){
        const theta = j * 2 * Math.PI / edge;
        const w = size * Math.cos(theta);  //奥行(z軸上の幅)
        const h = size * Math.sin(theta);  //縦
        const v = new THREE.Vector2(0, h); //回転させるためベクトルに変換
        v.add(bone[i]);                    //boneを中心に移動
        v.rotateAround(bone[i], angle);    //angle分回転
        pt[i][j] = [v.x, v.y, w];
    }
}

z-y平面上に半径=sizeの円を描き、z軸で回転させているイメージ。

この時、boneの座標を加算して、各円の回転の中心をboneに持ってきている。

メッシュの作成

前回の記事と同じく、BufferGeometryを使ってジオメトリを作成する。

以下は関数化してまとめたコードの全体。

//ページの読み込みを待つ
window.addEventListener('load', init);
function init() {

    ///////////////////////////////////////////////
    //           画面設定                   //
    //////////////////////////////////////////////

    //画面サイズを指定
    const width = window.innerWidth;
    const height = window.innerHeight;
    //レンダラーを作成
    const renderer = new THREE.WebGLRenderer({
        canvas: document.querySelector('#myCanvas')
    });
    renderer.setClearColor(new THREE.Color('grey'));
    document.body.appendChild( renderer.domElement );
    renderer.setSize(width, height);
    //シーンを作成
    const scene = new THREE.Scene();
    //カメラを作成
    const camera = new THREE.PerspectiveCamera(45, width / height);
    camera.position.set(-50, 50, 100);
    camera.lookAt(new THREE.Vector3(0, 0, 0));
    //ライトを設置
    const envlight = new THREE.AmbientLight(0xffffff, 1);
    scene.add(envlight);

    ///////////////////////////////////////////////
    //         マテリアル作成                  //
    ///////////////////////////////////////////////

  //パイプの設定
    const sect = 5;
    const edge = 12;
    const size = 5;
    const center2D = new THREE.Vector2();
    const ep = new THREE.Vector2( 10, 10 );
    const cp = new THREE.Vector2( 10, 0 );
    //パイプを作成
    const pt = makePipe(sect, edge, size, cp, ep);
  //メッシュの作成
    const geometry = makeGeometry(sect, edge, pt);
    const material = new THREE.MeshNormalMaterial({
        side:THREE.DoubleSide,
    });
    const plane = new THREE.Mesh( geometry, material );
    scene.add( plane );

    ///////////////////////////////////////////////
    //            共通関数                  //
    ///////////////////////////////////////////////

    //パイプ作成
    function makePipe(sect, edge, size, cp, ep){
        //make bone
        const curve = new THREE.QuadraticBezierCurve(center2D, cp, ep);
        const bone = curve.getPoints(sect);
        let zpos = bone[0];
        //set points
        const pt = [];
        for(let i=0; i<(sect+1); i++){
            //calc angle
            const diff = new THREE.Vector2().subVectors(bone[i], zpos);
            const angle = Math.atan2(diff.y, diff.x);
            //calc coords
            pt[i] = [];
            for(let j=0; j<edge; j++){
                const theta = j * 2 * Math.PI / edge;
                const w = size * Math.cos(theta);
                const h = size * Math.sin(theta);
                const v = new THREE.Vector2(0, h);
                v.add(bone[i]);
                v.rotateAround(bone[i], angle);
                pt[i][j] = [v.x, v.y, w];
            }
            zpos = bone[i];
        }
        return pt;
    }

  //カスタムvertexの作成
    function setVertices(sect, edge, pt){
        const vert = [];
        for(let i=0; i<sect; i++){
            vert[i] = [];
            for(let j=0; j<edge; j++){
                vert[i][j] = [];
                vert[i][j][0] = pt[i][j];
                vert[i][j][1] = pt[i][(j+1)%edge];
                vert[i][j][2] = pt[i+1][(j+1)%edge];
                vert[i][j][3] = pt[i+1][j];
            }
        }
        return new Float32Array(vert.flat(3));
    }

  //カスタムindexの作成
    function setIndices(sect, edge){
        const num_rect = sect * edge;
        const order = [0,3,2,2,1,0];
        const index = [];
        for(let i=0; i<num_rect; i++){
            for(let j=0; j<order.length; j++){
                index.push(order[j]+(4*i));
            }
        }
        return new Uint16Array(index);
    }

    //BufferGeometryの作成
    function makeGeometry(sect, edge, pt){
      const vertices = setVertices(sect, edge, pt);
      const indices = setIndices(sect, edge);
      const geometry = new THREE.BufferGeometry();
      geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
      geometry.setIndex(new THREE.BufferAttribute(indices, 1));
      geometry.computeVertexNormals();
      return geometry;
    }

    ///////////////////////////////////////////////
    //         レンダリング開始               //
    //////////////////////////////////////////////

    render();
    function render(){
        requestAnimationFrame(render);
        renderer.render(scene, camera);
    }
}

AR.js+three.jsでwebAR(1)

blenderで作成した3DモデルをARで表示するまで。

①blenderでモデルをglbとしてエクスポート

②three.jsのGLTFLoaderでモデルを読み込み

③AR.jsでモデルを表示

という流れ。

blenderでモデルをエクスポート

フォーマットはglTF Binary(.glb)が一番軽いのでこれを選択。

パラメータはとりあえず以下のチェックが入っていればテクスチャ等もそのまま出力される様子。

※この時マテリアルをDeffuse BSDFにしたところブラウザの方でうまくテクスチャが反映されなかったので、Principled BSDFのままのほうがいいっぽい。

html側の記述

<!DOCTYPE html>
<html>
    <head>
        <title>AR test</title>
        <meta name="viewport" content="width=device-width, user-scalable=no"/>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r123/three.min.js"></script>
        <script src="https://raw.githack.com/AR-js-org/AR.js/3.1.0/three.js/build/ar.js"></script>
        <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r92/examples/js/loaders/GLTFLoader.js"></script>
        <script src="three.js"></script>
    </head>
    <body style='margin : 0px; overflow: hidden;'></body>
</html>

・ビューポートの設定。画面をデバイスの大きさに合わせる。またスマホのズーム操作を禁止

・必要ソースの読み込み

・bodyタグで余白の除去とスクロールの禁止

three.js側の記述

まずARの表示に最低限必要な画面設定のみ

window.addEventListener("DOMContentLoaded", init);

function init() {
    //レンダラーの設定
    const renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true
    });
    renderer.setClearColor(new THREE.Color(), 0);
    renderer.setSize(640, 480);
    renderer.domElement.style.position = 'absolute';
    renderer.domElement.style.top = '0px';
    renderer.domElement.style.left = '0px';
    document.body.appendChild(renderer.domElement);
   
    //画面設定
    const scene = new THREE.Scene();
    scene.visible = false;
    const camera = new THREE.Camera();
    scene.add(camera);
    const light = new THREE.AmbientLight(0xFFFFFF, 1.0);
    scene.add(light);

    //画面リサイズの設定
    window.addEventListener('resize', () => {
        onResize();
    });
    function onResize() {
        arToolkitSource.onResizeElement();
        arToolkitSource.copyElementSizeTo(renderer.domElement);
        if (arToolkitContext.arController !== null) {
            arToolkitSource.copyElementSizeTo(arToolkitContext.arController.canvas);
        }
    };
      
    //AR周りの設定
    const arToolkitSource = new THREEx.ArToolkitSource({
        sourceType: 'webcam'
    });
    arToolkitSource.init(() => {
        setTimeout(() => {
            onResize();
        }, 2000);
    });
    const arToolkitContext = new THREEx.ArToolkitContext({
        cameraParametersUrl: 'data/camera_para.dat',
        detectionMode: 'mono'
    });
    arToolkitContext.init(() => {
        camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
    });
    
    //マーカー設定  
    const marker1 = new THREE.Group();
    scene.add(marker1);
    const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, marker1, {
        type: 'pattern',
        patternUrl: 'data/patt.hiro',
    });

    //レンダリング
    requestAnimationFrame(function animate(){
        requestAnimationFrame(animate);
        if (arToolkitSource.ready) {
            arToolkitContext.update(arToolkitSource.domElement);
            scene.visible = camera.visible;
        }
        renderer.render(scene, camera);
    });
}

arToolkitSource初期化時のリサイズ処理は2秒待つ処理を入れないと自分の環境ではうまくいかなかった(公式のexsampleでは意図的に待つ処理を入れている)。

モデルの読み込みと表示

GLTFローダーでモデルを読み込み、マーカーに対応させることで表示

      const gltfloader = new THREE.GLTFLoader();
      gltfloader.load('./data/apple.gltf',function(gltf){
          marker1.add(gltf.scene);
      });

・’./data/apple.gltf’はモデルデータのURL

・モデルそのものではなくシーンを追加していることに注意

three.jsでカスタム図形を作る(2)

前回シンプルな四角い板を作ったので、今回はそれを応用して多角柱を作ってみる。

作りたい形のイメージ(六角柱)

①図形の座標を計算する

多角形を作り、セクションの数ぶん画面の奥に伸ばしていくイメージ。

ここでは六角形を2セクションぶんの奥行きで作ってみることにする。

  const sect = 2; //sectionの数
  const edge = 6; //edgeの数
  const size = 10; //1辺の長さ
  
  const pt = []; //座標を入れる配列
  for(let i=0; i<(sect+1); i++){
      pt[i] = [];
      let z = -size * i; //奥行き
          for(let j=0; j<edge; j++){
              let theta = j*2*Math.PI / edge;
              let x = size * Math.cos(theta);
              let y = size * Math.sin(theta);
              pt[i][j] = [x, y, z];
      }
  }

座標はptに下図のような順番で入る。(pt[2][0]以降は省略)

②verticesの作成

座標がこの順番のままだと面が貼れないので、四角のブロックごとに区切っていくように座標の順番を変えてverticesを作る。

  const vert = [];
  for(let i=0; i<sect; i++){
      vert[i] = [];
      for(let j=0; j<edge; j++){
          vert[i][j] = [];
          vert[i][j][0] = pt[i][j];
          vert[i][j][1] = pt[i][(j+1)%edge]; //edgeを一周回ったら0に戻る
          vert[i][j][2] = pt[i+1][(j+1)%edge];
          vert[i][j][3] = pt[i+1][j];
      }
  }
  const vertices = new Float32Array(vert.flat(3)); //1次元配列に変換

下図のような順番に座標の順番を整理した後でverticesを作成している。

③indicesの作成

面を貼る順番が表側になるように(前回の記事参照)ひとつめの四角(0⇒3⇒2⇒2⇒1⇒0)ふたつめの四角(4⇒7⇒6⇒6⇒5⇒4)の要領で四角の数ぶんindexの順番を入れていく。

  const num_rect = sect * edge;
  const order = [0,3,2,2,1,0];
  const index = [];
  for(let i=0; i<num_rect; i++){
      for(let j=0; j<order.length; j++){
          indx.push(order[j]+(4*i));
    }
  }
  const indices = new Uint16Array(index);

④バッファーオブジェクトの作成

頂点座標とインデックスをアトリビュートするところは前回と一緒だが、さらにcomputeVertexNormals()で法線向きを計算してあげる必要がある。

 const geometry = new THREE.BufferGeometry();
  geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
  geometry.setIndex(new THREE.BufferAttribute(indices, 1));
  geometry.computeVertexNormals();

⑤メッシュを作成

あとは任意のマテリアルでメッシュを作成する。

結果が分かりやすいようにノーマルが色付けされるマテリアルを使って、ワイヤーフレーム表示させてみる。

  const material = new THREE.MeshNormalMaterial({
    side:THREE.DoubleSide, //面の裏側も表示する
    wireframe:true
  });
  const plane = new THREE.Mesh( geometry, material );

  scene.add( plane );

結果はこんな感じ。(正面からだと図形がわかりにくいのでカメラのポジションをちょっとずらしている)

さらにedgeの数を増やせば円錐に近づく。(カメラ左から見るとノーマルの色が変わる)

NEO SOULコードプログレッション:マイケルジャクソン『Butterflies』

最近ロバートグラスパーの曲が好きで良く聴いていたんですが、youtubeに「ロバートグラスパーみたいな、”ネオ・ソウル”っぽいコード進行ってどうやって出来ているのか?」を解説するチュートリアル動画が結構あって、面白そうだったので勉強がてらまとめてみようとおもいました。

※ちなみにわたしは「スケールとかコードとか、原理はなんとなくわかるけど全然身にはついてない」というレベルです。

※参考にしている動画は鍵盤で解説していますが、わたしの身近にある楽器はギターなのでギターに引き付けて考えてみてます。

まず、ネオソウルっぽさってうのはこのキーボードのパッドっぽいかんじのソフトな音色によるところがでかいんじゃないかなという気がする。これそのままギターで弾いてもなかなかこのイキフンが出ない。。

次に、「クラスター・コード」っていう重要っぽい観念が出てくる。これは、ネオソウルっていうのは大体全部のコードにばちばちにテンションを散らしているが、テンション音を高音域の方に重ねていくんじゃなくて、コードの基本形の中に置いちゃって、隣接した音をいっぱい弾いて響きをわざと「濁らす」「ダーティな音にする」ってことらしい。

そもそもギターだと隣接する音を同時に沢山鳴らすって難しいので、やっぱりこれもイキフンが出にくい。素直に鍵盤でやるべきか。。と思うも、ギターのコードフォームとか一緒に覚えたいので(いまだに指板のどこにその音があるのかうろ覚え)、無理やり頑張っていきたい。

Butterfliesのコードを学ぶ

動画の20分辺りから、マイケルジャクソンの『Butterflies』(原曲はこちら)のコード進行を解説しているので今回はそこをじっくり見ていく。

基本的なコード進行:

Ab⇒C#m⇒E⇒A7

コードの構成音

2番目のC#mは、本来であればメジャー(ダイアトニックコードでⅣはメジャー)のはずだけど、ここで「モードチェンジ」してコードのクオリティを変化させて雰囲気を変えている、と言っていた。

3番目のEは、そもそもAbメジャーの構成音じゃないからout of chordしてるんだけど、なんでオッケーなのかっていうと、

1)メロディのキーとなっている音Abがずっとトップで鳴っていて、統一感が出ているので違和感がない

2)C#mでモードチェンジした時に、 keyがBに変わった、と考えることができて、Bに対するEはⅣなのでEメジャーが使える

という二通りの考え方ができるらしい。

最後のA7。♭Ⅱ7というのはV7の「裏コード」というものらしく、ドミナントの代わりによく使われることがあるらしい。今日のところは深追いせずに「そうかあ」とおもっておく。

エクステンションを追加

テンションを加えてコードにバリエーションを付ける。

AbMaj9(13)⇒C#m9(11)⇒EMaj9⇒GMaj7(b5#5)⇒Ab6/9

コードの構成音

A7だったところがGMaj7(b5#5)になっているが、これはA9(13)11#のルートを弾かない(ルートレス)でGから積み上げていっている。ルートレスもネオソウルによく出てくる弾き方ということ。

最後に戻ってくるときにAb6/9とかバリエーションつけるとネオソウルっぽくてかっこいい。

ギターに置き換えるとこんな感じかな?

AMaj9(13)
C#m9(11)
EMaj9
GMaj7(b5#5)

うーん、どうなんでしょう。全部の音を詰め込もうとしないほうがこなれ感がでるのかもしれません。最後のコードは怪しい響きでかっこいい。

three.jsでカスタム図形を作る(1)

自分で座標を自由に作って図形を表示させる時のやり方。

先ずはシンプルな四角い板を作ってみる。

①verticesの作成

それぞれの頂点のx,y,z座標を配列に詰める。

  const positions = [
        5.0,  5.0, 0.0, //頂点0
        5.0, -5.0, 0.0, //頂点1
       -5.0, -5.0, 0.0, //頂点2
       -5.0,  5.0, 0.0  //頂点3
  ];
  const vertices = new Float32Array(positions);

②indicesの作成

次に、さっき指定した座標に面を貼っていく。面は三角形単位で貼る必要がある。この時、三角形を貼る順番によって法線の向きが変わる。反時計回りに貼った面は表側になり、その逆だと面が裏側になる。

表側の面を貼りたいので、頂点0⇒頂点3⇒頂点2、頂点2⇒頂点1⇒頂点0のように三角形の面を貼る順番をindices配列に入れていく。

  const order = [0,3,2,2,1,0];
  const indices = new Uint16Array(order);

③バッファーオブジェクトの作成

バッファーオブジェクトを作成し、さっき作った頂点座標とインデックス情報を与える。

  const geometry = new THREE.BufferGeometry();
  geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
  geometry.setIndex(new THREE.BufferAttribute(indices, 1));

positionにはvertices配列のデータが3つずつ入り、index番号に対応した順番で面が作られる。

④メッシュを作成してシーンに追加

あとは好きなマテリアルを選んでメッシュを作成してシーンに表示させることができる。

  const material = new THREE.MeshLambertMaterial({color: 0x6699FF});
  const plane = new THREE.Mesh( geometry, material );
  scene.add( plane );

three.jsでシェーダーを使う基本

three.jsを使って、板ポリに画像を表示させる手順の覚書。

まず、以下はthree.jsでブラウザにまっさらの画面を表示させるために必要な最小限の構成。

画面を表示する

three.js

//ページの読み込みを待つ
window.addEventListener('load', init);

function init() {

  //画面サイズを指定
  const width = 800;
  const height = 400;

  //レンダラーを作成
  const renderer = new THREE.WebGLRenderer({
        canvas: document.querySelector('#myCanvas')
      });
  renderer.setClearColor(new THREE.Color('grey'));//背景色の設定
  document.body.appendChild( renderer.domElement );
  renderer.setSize(width, height);

  //シーンを作成
  const scene = new THREE.Scene();

  //カメラを作成
  const camera = new THREE.PerspectiveCamera(45, width / height);
  camera.position.set(0, 0, 100);
  camera.lookAt(new THREE.Vector3(0, 0, 0));

  //ライトを設置
  const envlight = new THREE.AmbientLight(0xffffff, 1);
  scene.add(envlight);

 //レンダリング開始
  render();

  function render(){
      requestAnimationFrame(render);
      renderer.render(scene, camera);
  }
}

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <!-- 必要ソース読み込み -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script>
    <script src="three.js"></script>
  </head>
  <body>
    <canvas id="myCanvas"></canvas>
  </body>
</html>

シェーダーを作成する

さらに、<BODY>内にシェーダーの記述を追加する。以下は、メッシュの全ピクセルを赤一色で塗るだけのシェーダー。

<div id="webgl"></div>
<!-- 頂点シェーダー -->
<script id="vert" type="x-shader/x-vertex">
    void main() {
        //positionはShaderMaterialで補完されるジオメトリの頂点情報
        //カメラ座標に変換したものを最終的にgl_Positionに代入
        vec4 pos = modelViewMatrix * vec4(position, 1.0);
        gl_Position = projectionMatrix * pos;
    }
</script>
<!-- フラグメントシェーダー -->
<script id="frag" type="x-shader/x-fragment">
    void main() {
        //全ピクセルを赤にする
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
</script>

Thee.js側でプレーン(板)を追加し、マテリアルに↑で作ったシェーダーを指定すると、赤い板が出てくるはず。。

//(1)Planeジオメトリ(座標)を作成
const geometry = new THREE.PlaneGeometry( 15, 20, 1 );
//(2)マテリアル(材質)にShaderMaterialを指定する
//htmlからvertとfragのソースを読み込んで指定
const vert = document.getElementById('vert').textContent;
const frag = document.getElementById('frag').textContent;
const material = new THREE.ShaderMaterial({
    vertexShader: vert,
    fragmentShader: frag,
});
//(3)ジオメトリとマテリアルからメッシュを作成
const plane = new THREE.Mesh( geometry, material );
//(4)メッシュをシーンに追加
scene.add( plane );

シェーダーを使ってテクスチャを表示させる

表示させたいpng画像を配置(ここではimgフォルダ配下にfire.pngを配置)。テクスチャとして適用する。pngのアルファ値を反映させたい場合はtransparantをtrueにする。

//テクスチャローダーを作成し画像を読み込む
const loader = new THREE.TextureLoader();
const tex = loader.load('img/fire.png');

//uniform変数uTexとしてテクスチャをシェーダーに渡す
const material = new THREE.ShaderMaterial({
    uniforms: { uTex: { value: tex } }, //uTexとしてテクスチャ情報をセット
    transparent: true, //画像の透明度を有効に
    vertexShader: vert,
    fragmentShader: frag,
 });
<script id="vert" type="x-shader/x-vertex"> 
    varying vec2 vUv; //フラグメントシェーダーに頂点情報を渡す用の変数
    void main() {
        vUv = uv; //頂点情報を格納する
        vec4 pos = modelViewMatrix * vec4(position, 1.0);
        gl_Position = projectionMatrix * pos;
    }
</script>

<script id="frag" type="x-shader/x-fragment">
    varying vec2 vUv; //頂点シェーダから渡された変数
    uniform sampler2D uTex; //three.jsから渡されたテクスチャ情報
    void main() {
        vec4 color = texture2D( uTex, vUv ).rgba; //頂点ごとのテクスチャの色取得
        gl_FragColor = color;
    }
</script>

ここでブラウザのクロスプラットフォームを無効化しないと描画されないので注意。

SWに出会ってSWにお別れする話

TROSを観てきた。

ディズニーが言う「真実の愛」とは、「もののあはれ」の対義語である。
真実の愛による魔法のキスは、そんなわけで、バケモノを美しい男に変えたり、壊れたメモリの記憶を復活させたり、死んだ人間を生き返らせたりする。
この王国では、夏になっても雪だるまは溶けず、子供の頃のイマジナリーフレンドには、クローゼットを通じていつでも会える。
「キャラクター商品にお金を落とす限り、あなたはずっと子供のままでいていい」と、妖精が耳元で囁いてくれる。

わたしたちは回転木馬に乗っている。
ここでは誰もが王様で、目の前には次々と、自動的に、あらゆるお菓子が差し出される。
TROSがもしもキャンディの内のひとつだったら、甘いだの苦いだのと言えたことだろう。
だけどこれはキャンディではない。木馬のほうなのだ。
だからもはや、良いも悪いもない。あるのはただ「からくり」だけである。

この巨大な機械は一体、誰のために動いていたのだろう。単に止めどない創作意欲を昇華するため、はたまた、それは子供たちのため、特に、どこかの名もなき孤独な少女のため、または、世界から脇役に押しやられた人々のためだったのだろうか。
いまやそれは、臆病風に煽られて、慣性だけでくるくる回っている。

実際のところ、乗っているわたしたち自身が、ここで回り続けることに決めたのかもしれない。
そうだとしたら、わたしはもうそんなファンダムの一員でいたくない。黄色いロゴが印刷されたTシャツを着て、暗黙のうちに”仲間”と連携したくない。
いっそ雷をおとして、全て燃やしてしまうべきだろうか。
確かにわたしはSWを愛していた時があったし、夢を見て心躍った時があった。その記憶があれば充分ではないのか。
それに、きっと私が求めるSWは過去ではなく、常に未来にあるのだ。

こんな風に書いてはみたけれど、思い出してみると、わたしがSWを好きになったきっかけは、『シスの復讐』の冒頭のドッグファイトを見て「宇宙ってかっけー」と思ったから、というあまりにささやかな理由に過ぎなかった。

だからわたしが自分なりにSWを愛したように、きっと誰かもまたTROSを見て、誰かなりにそれを愛するのだとおもう。