Butadiene Works

ジンジャエールが飲みたいです@twitter:butadiene121

Houdiniなんもわからんマンが、Houdini初心者になるまで

はじめに

みなさんこんにちは。ブタジエンです。みなさん、Houdiniというソフトをご存知でしょうか?Houdiniは3DCGソフトウェアの一つで、「プロシージャルなモデリング」等を得意とする(らしい)ソフトです。僕もしばしば名前をよく聞いていて興味はあったのですが、日本語のチュートリアル等も(Blenderとかと比べると)少ない印象があり、なかなか手が付けられずにいました。

ところがこの夏ひょんなことから機運がわき、多少触ってこれぐらいのアウトプット(下に貼ったやつ)が出せるぐらいにはなったので、「Houdiniってなんだ?」状態の僕が、初心者になってちょっとした作品を出せるようになるまでの過程をまとめてみました。

f:id:butadiene:20191103200025p:plain

Houdiniの使い方、というよりは、ド素人がどんなふうに学習フローを組んだのか、という感じの記事になると思います。よろしくお願いします。

僕について

まず、Houdiniをやろうと思ったころ(2019年7月から8月)の僕のステータスをまとめておきます。

  • 僕(ブタジエン ブタジエン (@butadiene121) | Twitter

  • 地方の大学の3年生(専攻は物理っぽいところ 非情報系)

  • プログラミング歴一年弱(扱えるのはShaderのみ Unityのc#は少しだけ触れる)

  • VRChatのため、という用途のみ(すなわちc#スクリプトを書かない)でUnity歴1年

  • Blenderを少しだけ触ったことがある。(初心者用のサイトのチュートリアルを一周した程度)

  • 比較的強めのPCを持っている(いわゆるVRReadyと呼ばれるもの 具体的にはメモリ16G、CPU i7-7700HQ、GPU GTX1070)

  • 絵は一切書けない

  • インターネット上の知り合いでHoudiniを使ってる人が複数人いる

Houdiniについて

次に、Houdiniというソフトについて(僕の理解の範囲内で)まとめておきます

  • 3DCGソフト

  • プロシージャルなモデリングや、シミュレーションが得意

  • ノードベースでネットワークを作ってモデリングをしたりエフェクトを作ったりする

  • プログラムを使って自分でノードを作れたりいろいろできる。

  • HoudiniエンジンでUnityやUE4との連携が可能

諸々・・・

Houdiniに興味を持ったきっかけ

2019年の春ごろからHoudiniの名前をよく聞いていて、「プログラミングを使ってモデリングができる」みたいな話を聞いていて、とても興味がありました。

触ろうかなあと思っていた時にtanittaさんのVJを見る機会がありました。それがめちゃくちゃかっこよくて、話を聞いてみるとHoudiniを使って動画を作っているという話だったので、よし、やろうということになりました。2019年の7月とかの話だったと思います。

とりあえずインストール

Houdiniは無料版(Houdini Apprentice)があるとのことだったのでとりあえずインストールしてました。(Houdiniには機能はほぼ全部使えるが、出力画像にウォーターマークがついたり、FBX等が出力できないHoudini Apprenticeという名前の無料版がある) note.mu

チュートリアルを探せ

次に、チュートリアルを探しました。

自分がBlenderを勉強したときは、日本語のチュートリアルサイトを見て勉強したので、Houdiniでも日本語のチュートリアルサイトがあるのだろうと思い、探してみたのですが、これがなかなか見つかりませんでした・・・。

動画ベースなら、公式が大変丁寧なチュートリアルを出しています。

www.sidefx.com

ですが、自分はテキストで読んでいきたいタイプで、動画タイプのチュートリアルは苦手だったので、チュートリアルサイト的なのを探したのですがなかなか見つからず・・・。

どうにもなりそうになかったのですが、Houdiniを触ったことのあるシェーダーアーティストのやぎりさんから解説本はいいぞみたいな話を聞き、アマゾン等を漁るとHoudiniの解説本がたくさん見つかりました。これならば、と思い、やぎりさんのおすすめであるこちらの本であるこれを購入しました。

Houdini ビジュアルエフェクトの教科書

買った本を進める

この本は、Houdiniを初めて触る人でも扱える本で、たくさんの作例とそれの作り方と解説が、技術をきちんと習得していけるような順番で載っているタイプの本でした。

そのため、この本の最初から作例を丸写しするという形で進めていきました。

この本で扱ってるHoudiniのverとインストールしたHoudiniのverが違ったので(ちゃんと気を付けましょう)丸写しでうまくいかないこともまれにありましたが、基本的には本の内容を読んで、理解した「つもり」になって丸写しするだけで進めていくことができました。8月の上旬から中旬のことです。

この時期は夏休みのインターンと重なっていたので、インターンの業務が終わって家に帰ってから一日1時間みたいなペースで進めてました。

f:id:butadiene:20191007215438p:plain
買ったHoudini本に載っていた作例を丸写しして自分で作ったやつ①

f:id:butadiene:20191007215748j:plain
買ったHoudini本に載っていた作例を丸写しして自分で作ったやつ②

8月下旬はインターンが忙しくなってしまったので、Houdiniはお休みしてました。

プログラミングを書きたい

9月に入っても上に書いた本を進めていたのですが、この本を進めるだけではいつまでたってもプログラミングを書く機会がないことに気づきました。もともとHoudiniには「プログラミングでモデリングができるらしい」ということで興味を持っていたのですが、全然出てこない。で、よくよく調べてみると、HoudiniではVEXというプログラミング言語を使って様々なことができるらしいのですが、上の本はVEXを扱っていない、ということが分かりました。これは完全に僕のリサーチ不足です。(VEXが載っていない以外についてはすごくわかりやすく、良い本でした)

この時点で、上の本は半分強ぐらい終わっていて、基本的な操作やちょっとしたオブジェクトの作り方は一応わかるようになっていたので、ここから先はVEXを学ぼうと思い、再びインターネットの海をさまよいました。

ところが、でてこない。

VEXをわかりやすく説明した日本語のサイトが見つかりませんでした・・・(これは実際に「ない」のか私のググり力が弱いせいなのか、どちらかはわかりません。)

そこでツイッターでVEXの書き方のいいチュートリアルを尋ねたところ、とある本がおすすめだと教えてもらいました。

これは正直言って迷いました。この時期あんまりお金がなくて、上のチュートリアル本の時点ですでに4000円したんですよね。で、このVEXの書き方が載っている本は6000円以上します・・・。

が、やっぱりプログラミングがしてみたかったので買ってしまいました。

こちらの本です。

VEXの練習

こちらの本は、最初にHoudiniの使い方の説明をして、そのあとにプログラミングの基本的な話を説明、そのあとVEXの説明をした後で、VEXを使った作例がたくさん載っている、という感じの本でした。

Houdiniの使い方と、プログラミングの基本的な話は分かっていたので飛ばして、数時間ぐらいVEXの説明についてのんびり眺めました。 そのあとは、後ろにあるVEXを使った作例の中で楽しそうな奴を適当に選んで解説を読みながら丸写していきました。3つぐらいの作例について丸写しを行ったと思います。

9月入ってもインターンは続いていたので、インターンから帰った後の1~2時間ぐらいを使って書いてました。

f:id:butadiene:20191007223556j:plain
買ったHoudini本に載っていた作例を丸写しして自分で作ったやつ③

正直に言うと、考え方がShaderに似ていたのでかなり楽でした。実際に作例を通じて手を動かしながら、VEXをどのように扱えばいいかについて、何となくわかったような気がしました。

オリジナル作品を作ってみる

VEXの作例は3つぐらい作ったところで飽きたので(この時点で9月中旬ぐらい)、オリジナルの作品を一つ適当に作ってみることにしました。この作品は、某何かで使いそうだったので、ここでライセンスを買う必要が出てきました。(先ほども言った通り、Houdini Apprenticeでは出力にウォーターマークがついてしまう)

そこで、Houdini Indieというものを購入しました(1年で$269、2年だと$399で利用可能)。ウォーターマークはこれで取れます。また、制限付きですが商用利用も可です。詳しくはここ

また、Houdiniではデフォルトで「mantra」というレンダラーを使用します。これは優秀なのですが、CPUでレンダリングを行うので少し遅いんですよね。そこでGPUレンダリングができるRedShiftというものを購入しました($500)。

(この時点で先ほどの解説本なんか比にならないぐらいにお金突っ込んでるんですよね・・・ 9月下旬にインターンのお給料が入ったのでそれを突っ込みました。)

VEXのチュートリアル本で得た、形状をプログラミングで作成する技術と、最初に使った本に載っているボリューム周りの技術を組み合わせてこのような作品を2週間ぐらいで制作し、10月上旬ごろに完成しました。

以上がHoudiniで自作の作品を作るまでの大まかな記録になります。

思ったこと

  • Houdiniの解説本はいいぞ -少なくとも上にあげた2冊に関して言えば、最新版よりもhoudiniのverが古い以外に困ることはあまりありませんでした。初心者でもわかりやすいように情報がまとまっており、個人的にはGoogleの海に飛び込むよりはるかに楽でした。少々高いですが買う価値はあると思います。

  • 質問できる存在がいるのは本当にありがたい -Houdiniの勉強をするにあたって何を使えばいいのかわからないときにお勧めの本やサイトを教えてくれた人の存在は本当にありがたかったです。ありがとうございます。とくにtanittaさんには自作の作品を作る際、どうしてもわからないことがあった時にも頼らさせていただきました・・・。本当に助かりました。

  • Houdiniはいいぞ -ノードとコードでモデリングしていく感じは本当に楽しいです。プリレンダで扱いやすいという条件の付いたGLSLでのレイマーチングのコーディング、という感じもしました。めっちゃ楽しい。

  • RedShift早え -RedShift、高かったですが(mantraと比べて)早くて助かりました。 ただし、その分レンダリングに関する情報は自分で集めなければいけなくなったのでそれは大変でした。

  • PC大事 -Houdiniそのものはそこまで重くはないのですが、少しでも複雑なことをしようとするとPCのリソースを相当食いました。少々とはいえ、強めのPCを持っておいてよかったと思います。自分のPCでとくにきつかったのはメモリです。最後の宇宙船の作品はオブジェクトについてかなりポリゴン量増やしたこともあり、自PCの16Gのメモリでは少し厳しかったです。

終わりに

ここまで読んでいただきありがとうございます。なにかありましたらTwitterブタジエンまでお願いします。

TheWaveVRに行った話

最近巷で話題になった、TheWaveVRをプレイしてみました。

TheWaveVRとは、VR空間内でDJパフォーマンスができるVRSNSで、派手な演出なども可能、イベントによった形のVRSNSになっています。

どういうところなのかはいろいろな人が語ってくれていると思うので割愛します。

どちらかというと、今日お話ししたいのはTheWaveVRではなく、TheWaveVRをプレイして出てきた、僕のポエムです。いわゆる、お気持ち表明というやつです。



TheWaveVRには、事前に作られたパフォーマンスを体験するモード(パーティクルライブみたいなやつ)と、DJがリアルでライブする、というやつがあります。僕が行ってきたのはライブのほうです。

TheWaveVRに入るとまず、エントランス的なところに飛ばされます。ライブの時間がもうすぐだと聞いて始めた僕はわき目見振らずライブ会場への入り口を探しました。それっぽいのがあったので入ってみることに。するとその先ではDJライブの真っ最中でした。20人から30人ほどの人たちが音楽を楽しみ、真ん中でDJが曲をかけ、前評判の通り、ド派手なエフェクトが展開されました。

最初に思ったのは「思ったより「は」リッチじゃないな」ということ。映像で見ていると、すさまじいエフェクトが行われているという印象だったので、どれほど心を折られるのかな?と警戒していった分もあるせいか、「なるほどなるほどこういう感じなのか」という感じでした。ただ、FPSはたぶん90出ており、とても軽い(負荷的に)印象も持ちました(というか、負荷とパフォーマンスの両立、という意味ではすさまじい領域にいると思います)。その次に思ったのはアバターの統一性です。TheWaveVRは似たような数種類のアバターしかないので、みんなどれかという感じで、全く同じアバターの人もいました。ただ、アバター自体は結構抽象化されていて、不快感みたいなのもなく、むしろ好みのデザインでした。この、「デフォルトアバターしか使えない」というものに後にいろいろい深く考えさせられることになります。

こんな感じの雰囲気。アバターが数種類しかない。

しばらくすると、数人が僕のほうによってきます。フレンドボール?みたいなのを僕に寄せてくるのですが、どう対応すればいいかもよくわかりません。手を当ててみたりしても反応せず困っていました。すると一人が、「Trigger」といってくださったので手をボールにあててトリガーを引くとフレンド登録されました。なるほどなるほど、こういう風にしてフレンド登録をするのか、と。始めたばかりで右も左もわからない僕にはフレンドは大歓迎です。僕によってきた数人の人に見様見真似でフレンドリクエストを送ります。無事に承認してもらえました。ここで、自分の声が聞こえているのか気になりました。「自分がしゃべってるかどうか」や、「他人がしゃべっているかどうか」がわかるUIがぱっと見ないのでわからなかったのですね。一人の人に「Can you hear me?」と聞こえてますか?と聞くと聞こえているようで返事を返してくれました。そこから適当に英語(へたくそイングリッシュ)を駆使して会話を続けます。これが衝撃的でした。この会話の距離感がありえないほど気持ちよかったのです。それはVRChatを始めたころに味わった頃に感じたような心地よさに少しだけ似ていました。



私がVRChatを始めたころはハックツールの最盛期で、コミュニティの中にいる人たちはみんなプライベートにいました。そこにアクセスするのはとても難しく、日本人を求めてパブリックをさまようしかありませんでした。JapanTownにたどり着くと、そこには多くの日本人がいましたが、VRChatを始めて何か月、みたいな人はほとんどいませんでした(そもそも2018年1月の時点ではVRChatをはじめて長くてもひと月の人がほとんどだった)。みんなVRChatをはじめて3日です、とか今日はじめました、みたいなひとばかりでした。そこでの会話はどうやってVRChatを知ったのか、こんなアバターの人がいた、みたいなはじめたばかりのときにありがちな会話でしたが、とても心地よかったのを覚えてます。今振り返ると、たぶんそこには「VRChatに興味を持った人たち」が集まっていたのです。しかもみんなあったばかりの人たちなのに。見知らぬ人たちとの共通の話題として「VRChat」が存在し得ました。


今のVRChatではこういう雰囲気を感じるのは少し難しくなっていると思います。パブリックに行くと、見知らぬ人たちとは会えますが、VRChat歴の長い人も多く、いまさら「VRChatに興味を持っている」人は多くないです。多分多くの人にとって関心ごとは、アバターやワールドやイベントであって「VRChat」ではないと思いますから(もちろん新規の方もいらっしゃいますが、新規を経験者が囲うという光景が一般化していますので、割合も多分そういう比率なのでしょう)。またアバターやワールドといっても作っている人もいればめぐる人もいますし、ひとによってフォーカスする対象も違います。つまるところ共通の話題、が今のVRChatのパブリックでは成立しにくいのです。たぶん(これは僕の主観です)。ちなみに、これは悪いことではないと思っていまして、VRChatはもうコンテンツではない、ということはVRChatは日常のインフラとして成立しているということですね。すごすぎることだと思いますし、諸手を挙げて歓迎すべきでしょう。

さて、ではパブリックではないところ、フレンドプラスや一部のフレンドオンリーなどはどうでしょうか? VRChatはシステムとして「人にjoinする」というシステムを取っています。つまり会いたい人のところに会いに行くわけであって見知らぬ人たちに会えるわけではないです。この話題について話したいときににはこの人のところに行けばいい、みたいなことは可能ですが、新しい人と話せる確率、つまり未知との遭遇率が減るわけです(逆にいうと、フレンドのフレンドみたいな人と初めまして、ができると話題に共通性がある可能性があり、ヒットすれば「求めてた」状況にはなりえますね。)。



こういうのを解決するにはどうすればいいかというと、一つの策として「場に集まる」というのが挙げられます。VRChatにおけるバーやクラブにはそういうところを(できる限り)になっている側面があると思います。ただし、未知との遭遇率はそれなり、という感じです(フレンドプラス等である以上仕方がない。)

たぶん、ファンタジー系のワールドに行けばファンタジーが好きな人たちが集まっていて、宇宙船ワールドに行けば宇宙の好きな人たちに会える、そういうのがわりと理想だったのかもしれませんがVRChatはそうなっていないですし、これからなることもないでしょう。

ちなみにじゃあ現実は?と聞かれてもそういうところはあまり思いつきません。強いて言うならクラブでしょうか・・・。クラブはそういうところだと聞いています(詳しくない)。というか現実でそういう見知らぬ人との会話を要求する行為をするのは普通は危険だったりもしますね、はい。



なるほど、僕がツイッターを好きになるわけです。



ではTheWaveVRは?と聞かれると、これがかなり良くできていると思うのです。つまりそのジャンルの音楽やパフォーマンスに興味がある人達が「場」に集まることができるわけです。また、ライブ、という形をとることで、人を集めることができます(24時間空いててもしょうがない)。結果としてある程度の人数が「人」ではなく「場」に集まることに成功しているわけです。また、アバターがないことも「没個性」を起こせていると思います。誰かに個性があるわけではないので、「みんなが興味あること」を「みんなで体験する」という形がとても尊重されて、それ以外がそぎ落とされているように思います(たとえば、ライブが主役なのに、誰かのかっこいいアバターがライブを置いて注目を集めることがない)。また、システム側にもそれをうまく促進するような仕組みがあって、TheWaveVRではライブ中にいくつかの公式の用意したエフェクトを自分で出すことができるのですが、近くに同じエフェクトを使っている人がいると、自分とその人のエフェクトが連携をします。まるっきり赤の他人と連携をしてもしょうがないわけですが、「この場に来ている(=自分の興味のあるこのパフォーマンスに興味がある人)」知らない人と、このような形で軽いコミュニケーションをとれるのは最高にクールだなと思うわけです。ほかにも、一緒にトリップする、なんていうエフェクトもあり、これも同様に良いなと思うわけです。また、アバターになにか仕込めるわけでもないので治安の維持も容易ですし、その結果「ライブ会場に誰でもはいることができる」ことを可能にするわけです。


ちなみに、じゃあVRライブはどうなんだ、という話をされるかもしれませんが、ここで一番着目しているのは「場を媒体にした交流」であって「場」そのものではないので、今回は割愛します。(多分アレほかの人と会話できませんよね・・?)

ここから先は理想論になるんですが、VRSNSの特質の一つとして「安全性」が挙げられると思うのです。つまり知らない人と話して、いくら盛り上がっても安全なわけです(すくなくともリアルよりは格段に)。 VRChatはVR空間で生きる、という未来を提示してくれましたが、もしかしたらこういう形のVRSNSは、「知らない人たち」と「興味のあることで盛り上がる」チャンスが「24時間いつでもどこでも、「何も」いらずに」ある、というリアルではなかなかないかもしれないものを提示してくれるのかもなあと思ってしまいます(しかも世界中から集まれるのでマイナーな話題でも行ける)。おそらくモラルの問題を考えると敷居が存在しなければいけないとは思うのですが、それでも何かを期待してしまいます

と、まあゴリゴリにTheWaveVRを推してきたわけなのですが、人が増えたり減ったりしたらどうなるかはわかりませんね。というか初期のVRChatが割とこうだったのでは?と先人の話を聞いて思うので、これはTheWaveVRが理想なのではなく、これぐらいの規模のVRSNSが理想なのでは見たいな話に落ち着きかねないのでアレなのですが。まあ、はい。よろしければみなさんやってみてください。土曜の午後とかならアメリカ金曜夜なのでいいと思います(たぶん)。

おしまい。

UnityでShaderを使ってカラフルなメタボール作ってみる。

はじめに

みなさんこんにちは。ブタジエンです。

今回レイマーチングの勉強としてUnityでShaderを使ってメタボールを作ってみました。

これはおもにShaderで実装されています。結構見てて楽しい表現なのかな・・・?

レイマーチングの勉強として作ったのですが、いろいろな基礎の技術要素が入った良い感じのShaderになったので自分の備忘録もかねて解説記事を書いてみました。

また、ある程度複雑なレイマーチング(ライティングが入っていたり、(一つか二つの式で表せるような)単純な図形ではない)のサンプルになるとも思います。

触れる内容はメタボールの(自分なりの)作り方、色の付け方、四面体ベースの法線、レイマーチングでの影、フォン鏡面反射、enhanced sphere tracing 、オブジェクトスペースでのレイマーチング、そしてデプスの書き込みになります。

【注意】この記事は専門家でない私が勉強したものをまとめたものです。間違いなどがあると思いますがその場合はブタジエン (@butadiene121) | Twitterまで指摘等頂けると幸いです。

今回のサンプルプロジェクトはGitHubにあげてあります。
github.com
プロジェクトの環境はUnity2018.2.10.f1ですが、Shaderしか入ってないのでまあだいたいどのバージョンでも動くと思います。

想定している読者層

Unityでレイマーチングを触ったことがある人、を想定しています。

サンプルについて

上に貼ったプロジェクト開けてmetaballsampleフォルダの中の"sample scene"を開ければわかると思います。

コード本体

以下が今回書いたShaderのコードになります。

github.com

// The MIT License
// Copyright © 2019 Butadiene
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// Use directional light
Shader "Butadiene/metaball"
{
	Properties
	{
		_ypos("floor height",float)=-0.25
		}
	SubShader
	{
		Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
		LOD 100
		Cull Front
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			uniform float _ypos ;
			#include "UnityCG.cginc"
			// The MIT License
			// Copyright © 2013 Inigo Quilez
			// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
			//Making noise
			float hash(float2 p)  
			{
				p  = 50.0*frac( p*0.3183099 + float2(0.71,0.113));
				return -1.0+2.0*frac( p.x*p.y*(p.x+p.y) );
			}

			float noise( in float2 p )
			{
				float2 i = floor( p );
				float2 f = frac( p );
	
				float2 u = f*f*(3.0-2.0*f);

				return lerp( lerp( hash( i + float2(0.0,0.0) ), 
								 hash( i + float2(1.0,0.0) ), u.x),
							lerp( hash( i + float2(0.0,1.0) ), 
								 hash( i + float2(1.0,1.0) ), u.x), u.y);
			}			
			///////////////////////////////////////////////////////////////////////
											
			float smoothMin(float d1,float d2,float k)
			{
				return -log(exp(-k*d1)+exp(-k*d2))/k;
			}
						
			// Base distance function
			float ball(float3 p,float s)
			{
				return length(p)-s;
			}

			
			// Making ball status
			float4 metaballvalue(float i)
			{
				float kt = 3*_Time.y*(0.1+0.01*i);
				float3 ballpos = 0.3*float3(noise(float2(i,i)+kt),
					noise(float2(i+10,i*20)+kt),noise(float2(i*20,i+20)+kt));
				float scale = 0.05+0.02*hash(float2(i,i));
				return  float4(ballpos,scale);
			}
			// Making ball distance function
			float metaballone(float3 p, float i)
			{	
				float4 value = metaballvalue(i);
				float3 ballpos = p-value.xyz;
				float scale =value.w;
				return  ball(ballpos,scale);
			}

			//Making metaballs distance function
			float metaball(float3 p)
			{
				float d1;
				float d2 =  metaballone(p,0);
				for (int i = 1; i < 6; ++i) {
				
					d1 = metaballone(p,i);
					d1 = smoothMin(d1,d2,20);
					d2 =d1;
					}
				return d1;
			}
		
			// Making distance function
			float dist(float3 p)
			{	
				float y = p.y;
				float d1 =metaball(p);
				float d2 = y-(_ypos); //For floor
				d1 = smoothMin(d1,d2,20);
				return d1;
			}


			//enhanced sphere tracing  http://erleuchtet.org/~cupe/permanent/enhanced_sphere_tracing.pdf

			float raymarch (float3 ro,float3 rd)
			{
				float previousradius = 0.0;
				float maxdistance = 3;
				float outside = dist(ro) < 0 ? -1 : +1;
				float pixelradius = 0.02;
				float omega = 1.2;
				float t =0.0001;
				float step = 0;
				float minpixelt =999999999;
				float mint = 0;
				float hit = 0.01;
					for (int i = 0; i < 60; ++i) {

						float radius = outside*dist(ro+rd*t);
						bool fail = omega>1 &&
							step>(abs(radius)+abs(previousradius));
						if(fail){
							step -= step *omega;
							omega =1.0;
						}
						else{
							step = omega * radius;
						}
						previousradius = radius;
						float pixelt = radius/t;
						if(!fail&&pixelt<minpixelt){
							minpixelt = pixelt;
							mint = t;
						}
						if(!fail&&pixelt<pixelradius||t>maxdistance)
						break;
						t += step;
					}
				
					if ((t > maxdistance || minpixelt > pixelradius)&&(mint>hit)){
					return -1;
					}
					else{
					return mint;
					}
				
			}

			// The MIT License
			// Copyright © 2013 Inigo Quilez
			// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
			// https://www.shadertoy.com/view/Xds3zN

			//Tetrahedron technique  http://iquilezles.org/www/articles/normalsSDF/normalsSDF.htm
			float3 getnormal( in float3 p)
			{
				static const float2 e = float2(0.5773,-0.5773)*0.0001;
				float3 nor = normalize( e.xyy*dist(p+e.xyy) +e.yyx*dist(p+e.yyx) 
					+ e.yxy*dist(p+e.yxy ) + e.xxx*dist(p+e.xxx));
				nor = normalize(float3(nor));
				return nor ;
			}
			////////////////////////////////////////////////////////////////////////////

			// Making shadow
			float softray( float3 ro, float3 rd , float hn)
			{
				float t = 0.000001;
				float jt = 0.0;
				float res = 1;
				for (int i = 0; i < 20; ++i) {
					jt = dist(ro+rd*t);
					res = min(res,jt*hn/t);
					t = t+ clamp(0.02,2,jt);
				}
				return saturate(res);
			}
			
			// The MIT License
			// Copyright © 2013 Inigo Quilez
			// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
			// https://www.shadertoy.com/view/ld2GRz

			float4 material(float3 pos)
			{
				float4 ballcol[6]={float4(0.5,0,0,1),
								float4(0.0,0.5,0,1),
								float4(0,0,0.5,1),
								float4(0.25,0.25,0,1),
								float4(0.25,0,0.25,1),
								float4(0.0,0.25,0.25,1)};
				float3 mate = float3(0,0,0);
				float w = 0.01;
					// Making ball color
					for (int i = 0; i < 6; ++i) {
						float x = clamp( (length( metaballvalue(i).xyz - pos )
								-metaballvalue(i).w)*10,0,1 ); 
						float p = 1.0 - x*x*(3.0-2.0*x);
						mate += p*float3(ballcol[i].xyz);
						w += p;
					}
				// Making floor color
				float x = clamp(  (pos.y-_ypos)*10,0,1 );
				float p = 1.0 - x*x*(3.0-2.0*x);
				mate += p*float3(0.2,0.2,0.2);
				w += p;
				mate /= w;
				return float4(mate,1);
			}
			////////////////////////////////////////////////////
			
			//Phong reflection model ,Directional light
			float4 lighting(float3 pos)
			{	
				float3 mpos =pos;
				float3 normal =getnormal(mpos);
				
				pos =  mul(unity_ObjectToWorld,float4(pos,1)).xyz;
				normal =  normalize(mul(unity_ObjectToWorld,float4(normal,0)).xyz);
					
				float3 viewdir = normalize(pos-_WorldSpaceCameraPos);
				half3 lightdir = normalize(UnityWorldSpaceLightDir(pos));				
				float sha = softray(mpos,lightdir,3.3);
				float4 Color = material(mpos);
				
				float NdotL = max(0,dot(normal,lightdir));
				float3 R = -normalize(reflect(lightdir,normal));
				float3 spec =pow(max(dot(R,-viewdir),0),10);

				float4 col =  sha*(Color* NdotL+float4(spec,0));
				return col;
			}

			
		
				struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				float3 pos : TEXCOORD1;
				float4 vertex : SV_POSITION;
			};

			
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.pos= mul(unity_ObjectToWorld,v.vertex).xyz;
				o.uv = v.uv;
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			struct pout
			{
				float4 pixel: SV_Target;
				float depth : SV_Depth;

			};

			pout frag (v2f i) 
			{
				float3 ro = mul( unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz;
				float3 rd = normalize(mul( unity_WorldToObject,float4(i.pos,1)).xyz
					-mul( unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz); 
				float t = raymarch(ro,rd);
				fixed4 col=0;

				if (t==-1) {
				clip(-1);
				}
				else{
				float3 pos = ro+rd*t;
				col = lighting(pos);
				}
				pout o;
				o.pixel =col;
				float4 curp = UnityObjectToClipPos(float4(ro+rd*t,1));
				o.depth = (curp.z)/(curp.w); //Drawing depth

				return o;
				
			}
			ENDCG
		}
		
	}
}

少し長いですが簡単に解説していきます。

コード解説

コードの構造

コードの流れとしては

  1. ライセンスやプロパティなどを記述
  2. ノイズ関数を記述(hash、noise)
  3. 球や床の接続などに使う関数を記述(smoothMin)
  4. 球の距離関数を記述(ball)
  5. ボールのステータスを作る関数を記述(metaballvalue)
  6. 球の距離関数とボールのステータスを受けてボールの距離関数を記述(metaballone)
  7. 上を受けてボール群の距離関数を記述(metaball)
  8. 床とボール群の距離関数を組み合わせて、最終的な距離関数を記述(dist)
  9. レイマーチングを行う関数の記述(raymarch)
  10. 法線を求める関数の記述(getnormal)
  11. 影を付ける関数の記述(softray)
  12. 球と床のマテリアルの色を決める関数の記述(material)
  13. ライティングをする関数の記述(lighting)
  14. シェーダ本体の記述(vert、frag)

という感じの構成になっています

コードに載せた注意書き

// Use directional light

ディレクショナルライトを使えということです。
僕にはまだ多様な光源に対応する能力がありませんのでとりあえずディレクショナルライト想定で作ってみました。

プロパティ

Properties
	{
		_ypos("floor height",float)=-0.25

		}

ここで床の高さを指定できるようにしています。

ノイズ関数

//Making noise
float hash(float2 p)  
{
	p  = 50.0*frac( p*0.3183099 + float2(0.71,0.113));
	return -1.0+2.0*frac( p.x*p.y*(p.x+p.y) );
}

float noise( in float2 p )
{
	float2 i = floor( p );
	float2 f = frac( p );
	
	float2 u = f*f*(3.0-2.0*f);

	return lerp( lerp( hash( i + float2(0.0,0.0) ), 
					 hash( i + float2(1.0,0.0) ), u.x),
				lerp( hash( i + float2(0.0,1.0) ), 
					 hash( i + float2(1.0,1.0) ), u.x), u.y);
}			

https://www.shadertoy.com/view/lsf3WH よりiqさんのノイズを使わせてもらっています シンプルなノイズです。今回はボールの移動に使わせてもらいました。


球や床の前後判定や接続

float smoothMin(float d1,float d2,float k)
{
	return -log(exp(-k*d1)+exp(-k*d2))/k;
}

ボールとボール・ボールと床が溶けて液体のようにつながるのに必要な関数です。 https://wgld.org/d/glsl/g016.html が詳しいと思います。

球の距離関数

// Base distance function
float ball(float3 p,float s)
{
	return length(p)-s;
}

ベースとなる球の距離関数を作成しています。参考:https://wgld.org/d/glsl/g009.html


ボールの位置、半径の決定とiとの紐づけ

// Making ball status
float4 metaballvalue(float i)
{
	float kt = 3*_Time.y*(0.1+0.01*i);
	float3 ballpos = 0.3*float3(noise(float2(i,i)+kt),
		noise(float2(i+10,i*20)+kt),noise(float2(i*20,i+20)+kt));
	float scale = 0.05+0.02*hash(float2(i,i));
	return  float4(ballpos,scale);
}

ボールのステートを作成する関数です。具体的にはfloat4(ボールの座標、ボールの大きさ)という形で出力します。
メタボールということで複数のボールを扱う必要があるわけですが、一つ一つのボールを数値iで区別して、(距離関数や色付けのところで)for文で全部まとめて出力してやろうという魂胆になっています。
ボールの位置や大きさはiを引数としてノイズや乱数で出力しています。

ボールのステータスを反映した距離関数

// Making ball distance function
float metaballone(float3 p, float i)
{	
	float4 value = metaballvalue(i);
	float3 ballpos = p-value.xyz;
	float scale =value.w;
	return  ball(ballpos,scale);
}

各ボールとの距離関数です。入力されたiをもとにしてボールのステータスを作成し、そこから距離を求めます。(日本語力が虚無になっていく・・)(そもそもこういうコードなんて上から読むものじゃない)

メタボール(ボール群)の距離関数

//Making metaballs distance function
float metaball(float3 p)
{
	float d1;
	float d2 =  metaballone(p,0);
	for (int i = 1; i < 6; ++i) {
				
		d1 = metaballone(p,i);
		d1 = smoothMin(d1,d2,20);
		d2 =d1;
		}
	return d1;
}

メタボール(ボール群)の距離関数です。ボールとの距離d1を求めてそれを1ループ前に計算したボールの距離d2とsmoothMinで比較するということをしてます。これによって6つのボール、そしてそれらが一部つながった状態のオブジェクトとの距離が出力されます。

最終的な距離関数

// Making distance function
float dist(float3 p)
{	
	float y = p.y;
	float d1 =metaball(p);
	float d2 = y-(_ypos); //For floor
	d1 = smoothMin(d1,d2,20);
	return d1;
}

最終的な距離関数です。メタボールまでの距離d1と床までの距離d2をsmoothMinで比較しています。これによって6つのボール群と床、そしてそれらが一部つながった状態のオブジェクトとの距離が出力されます。
なお、床の距離関数は

float d2 = y-(_ypos); //For floor

です。

レイマーチングを行う

求めた距離関数を使ってレイマーチングを実際に行う関数です
enhanced sphere tracing という論文を参考にしています。http://erleuchtet.org/~cupe/permanent/enhanced_sphere_tracing.pdf
簡単に言えば、レイを大きくすすめて通り過ぎてしまったときに少し戻る、というシステムによってレイを遠くへ届かせる技術と(前半部)、レイがオブジェクトすれすれを通ることによりループを消費してしまい衝突すべき点に衝突する前にループが終わって衝突していない判定になってしまう事象への対策のために、(スクリーンから見て)最もオブジェクトに近かった点を衝突点の候補とする、という技術(後半部)です。
※今回の場合、enhanced sphere tracingを使う必要性はそこまでないのですが(多分)、練習として組み込んでいます。

float raymarch (float3 ro,float3 rd)
{
	float previousradius = 0.0;
	float maxdistance = 3;
	float outside = dist(ro) < 0 ? -1 : +1;//レイのスタート地点が物体の中にあるか外にあるか
	float pixelradius = 0.02;
	float omega = 1.2;
	float t =0.0001;
	float step = 0;
	float minpixelt =999999999;
	float mint = 0;
	float hit = 0.01;
		for (int i = 0; i < 60; ++i) {

			float radius = outside*dist(ro+rd*t);
			bool fail = omega>1 &&
				step>(abs(radius)+abs(previousradius));
			if(fail){
				step -= step *omega;
				omega =1.0;
			}
			else{
				step = omega * radius;
			}
			previousradius = radius;
			float pixelt = radius/t;
			if(!fail&&pixelt<minpixelt){
				minpixelt = pixelt;
				mint = t;
			}
			if(!fail&&pixelt<pixelradius||t>maxdistance)
			break;
			t += step;
		}
				
		if ((t > maxdistance || minpixelt > pixelradius)&&(mint>hit)){
		return -1;
		}
		else{
		return mint;
		}
				
}

前半部のロジックについて(レイを大きくすすめて通り過ぎてしまったときに少し戻る というやつ)
最初のほうはomega=1.2がかかることによってレイが早く進むようになってます。

failの判定について。stepはここでは1ループ前に進んだ距離になっています。このstepの値が今進もうとしている距離radiusと1ループ前に進もうとした距離previousradius(omegaがかかっていないためstepと異なる値であることに注意)の和より大きければ進みすぎなので、omegaを1.0にして普通に進むこととします。なお、一度1.0が代入されるとomega> 1に常時はじかれるので以後はomega = 1.0のただのレイマーチングになります。

後半部のロジックについて((スクリーンから見て)最もオブジェクトに近かった点を衝突点の候補とするというやつ)
pixeltで求められてるのは、スクリーンに対するレイの進む半径の大きさです。minpixelt にはpixeltの最小値が、mintにはpixelt が一番小さい時のtの値が記録されます(上から二つ目のif文)。

ループの終了に関しては最大距離に加えて、「スクリーンに対するレイの進む半径の大きさ(すなわちpixelt)」がピクセルの大きさより小さいのならばループを終わってもよい。というのが加わっているのがわかると思います(上から3つ目のif文)。

ループを抜けた後はレイが衝突しているか否かの判定をするわけですが「スクリーンに対するレイの進む半径の大きさ(pixelt)」の最小値がピクセルの大きさより大きく、かつpixelt が一番小さい時のtの値mintが一定の値(ここではhit)より大きいときは当たっていない判定とします。当たっている判定とされたのなら、「スクリーンに対するレイの進む半径の大きさ(pixelt)」が一番小さかった時のt(すなわちpixelt が一番小さい時のtの値mint)を距離とします。

まあぶっちゃけ上で示した論文読むほうがいいと思います、はい。(私もあまり自信がありません・・・)
あと、この関数は最終的に視点からの距離を出力するわけですが、当たらなかったときは-1を返すようにしています。

四面体ベースで法線を求める

float3 getnormal( in float3 p)
{
	static const float2 e = float2(0.5773,-0.5773)*0.0001;
	float3 nor = normalize( e.xyy*dist(p+e.xyy) +
 		e.yyx*dist(p+e.yyx) + e.yxy*dist(p+e.yxy ) + e.xxx*dist(p+e.xxx));
	nor = normalize(float3(nor));
	return nor ;
}

法線を四面体ベースで求めることで距離関数を呼び出す回数を少なくしています。(普通にxyzで法線を求めると6回距離関数を呼び出すことになるがこれは4回) iqさんのコードを使わせてもらっています。
参照先はここです。http://iquilezles.org/www/articles/normalsSDF/normalsSDF.htm


影をつける

以下の関数では影を作ります。衝突地点からレイを光源の方向に飛ばし、なにかと当たった判定が出れば暗くします(つまりroにはレイの到達地点、rdには光の方向を入れます)。hnおよびその周りは影の周囲をぼかすための値です。(すれすれを通ってるとresに1と0の間の数値が出力される)

// Making shadow
float softray( float3 ro, float3 rd , float hn)
{
	float t = 0.000001;
	float jt = 0.0;
	float res = 1;
	for (int i = 0; i < 20; ++i) {
		jt = dist(ro+rd*t);
		res = min(res,jt*hn/t);
		t = t+ clamp(0.02,2,jt);
	}
	return saturate(res);
}

 

球と床のマテリアルの色の決定

色を作ります
https://www.shadertoy.com/view/ld2GRzを参考にしてボールの色を作っています。ただし、そのままでは動かなかったし、原理もわからないところがあったので少しいじっています。
(具体的には236行目の float x = clamp( length( blobs[i].xyz - pos )/blobs[i].w, 0.0, 1.0 ); が謎 なぜそこで割るのだ・・・。(多分想定しているblobsが少し違う気がする))

※この話なんですがphi16さんから大きい球はグラデーションに対する影響力がでかいように計算しているのでは、との話を伺いました。なるほど。

float4 material(float3 pos)
{
	float4 ballcol[6]={float4(0.5,0,0,1),
					float4(0.0,0.5,0,1),
					float4(0,0,0.5,1),
					float4(0.25,0.25,0,1),
					float4(0.25,0,0.25,1),
					float4(0.0,0.25,0.25,1)};
	float3 mate = float3(0,0,0);
	float w = 0.01;
		// Making ball color
		for (int i = 0; i < 6; ++i) {
			float x = clamp( (length( metaballvalue(i).xyz - pos )
					-metaballvalue(i).w)*10,0,1 ); 
			float p = 1.0 - x*x*(3.0-2.0*x);
			mate += p*float3(ballcol[i].xyz);
			w += p;
		}
	// Making floor color
	float x = clamp(  (pos.y-_ypos)*10,0,1 );
	float p = 1.0 - x*x*(3.0-2.0*x);
	mate += p*float3(0.2,0.2,0.2);
	w += p;
	mate /= w;
	return float4(mate,1);
}

一番最初にballcol[6]に各ボールの色を叩き込みます。
意味が分からんコードを自分なりに改編したコードがfor文の中身です。レイの最終地点が「ボールより0.1外側」より内側だった場合、色を描画するようにしています(だんだん濃くなってきてボールの表面より内側では完全にボールの色を使用)。xはこのままでは球の表面からその外側まで線形に色が落ちていく感じとなってしまうのでpにしていい感じの変化になるようにしています。mate += p*float3(ballcol[i].xyz);のところで全ボールの色を足していきます(重なってるところは色が足され、そうでないところはその場所にあるボールの色のみが表示される)。wでどれぐらい足したかを計測し、最後に割ることで、重なってる部分が明るくなるのではなくブレンドされるようになります。for文から抜けたら同じ要領で床の色も作ってしまいます。

ライティング

ライティングです。影と色を呼び出してきて、フォン鏡面反射と合わせて最終的な色を決めています フォン鏡面反射に関してはここが詳しいです。
http://nn-hokuson.hatenablog.com/entry/2016/11/04/104242
なお、レイマーチングには後述の通り、オブジェクトスペースを採用しているのですが、ライティングの計算のときはなにかとワールドスペースのほうが都合がいいのでその変換が最初に入ってます。

//Phong reflection model ,Directional light
float4 lighting(float3 pos)
{	
	float3 mpos =pos;
	float3 normal =getnormal(mpos);
	
	pos =  mul(unity_ObjectToWorld,float4(pos,1)).xyz;
	normal =  normalize(mul(unity_ObjectToWorld,float4(normal,0)).xyz);
					
	float3 viewdir = normalize(pos-_WorldSpaceCameraPos);
	half3 lightdir = normalize(UnityWorldSpaceLightDir(pos));	
	float sha = softray(mpos,lightdir,3.3);
	float4 Color = material(mpos);
				
	float NdotL = max(0,dot(normal,lightdir));
	float3 R = -normalize(reflect(lightdir,normal));
	float3 spec =pow(max(dot(R,-viewdir),0),10);

	float4 col =  sha*(Color* NdotL+float4(spec,0));
	return col;
}

  

シェーダ本体

出力です レイマーチングにはオブジェクトスペースを採用しています。
このシェーダを適用したオブジェクトのローカル座標において、視点の位置をレイの出発点とし、メッシュの位置と視点の位置を結ぶベクトルをレイの進む方向としています。
レイが衝突していない場合はクリップしています。最後にデプスを書き込んでおしまいです。お疲れ様でした!!

	struct appdata
{
	float4 vertex : POSITION;
	float2 uv : TEXCOORD0;
};

struct v2f
{
	float2 uv : TEXCOORD0;
	float3 pos : TEXCOORD1;
	float4 vertex : SV_POSITION;
};

			
			
v2f vert (appdata v)
{
	v2f o;
	o.vertex = UnityObjectToClipPos(v.vertex);
	o.pos= mul(unity_ObjectToWorld,v.vertex).xyz;
	o.uv = v.uv;
	UNITY_TRANSFER_FOG(o,o.vertex);
	return o;
}
			
struct pout
{
	float4 pixel: SV_Target;
	float depth : SV_Depth;

};

pout frag (v2f i) 
{
	float3 ro = mul( unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz;
	float3 rd = normalize(mul( unity_WorldToObject,float4(i.pos,1)).xyz
			-mul( unity_WorldToObject,float4(_WorldSpaceCameraPos,1)).xyz); 
	float t = raymarch(ro,rd);
	fixed4 col=0;

	if (t==-1) {
	clip(-1);
	}
	else{
	float3 pos = ro+rd*t;
	col = lighting(pos);
	}
	pout o;
	o.pixel =col;
	float4 curp = UnityObjectToClipPos(float4(ro+rd*t,1));
	o.depth = (curp.z)/(curp.w); //Drawing depth

	return o;
				
}		

 

課題

Unityのシーン上に影を「落とす」オブジェクトが存在した場合、うまく対応できません。(まあなんか原理的に無理な気もしなくもないですが・・・ Unityの影まったくわかんない・・・)

終わりに

今回はレイマーチングの勉強としてメタボールを作りました。いろいろな技術が扱えていい練習になったと思います。しかしレイマーチングは重いですね・・・。最適化法をもっと勉強してVRでも気軽にレイマーチングできるようになりたいです。

なにかあればブタジエン (@butadiene121) | Twitterまでご連絡ください。フィードバック等お待ちしております。

補記

  1. ライティング周りに不足していた点があり、バージョンが上がると動かないという指摘を受けましたので訂正をしました(2019/2/23)
  2. floating point division by zero 128というエラーが出ているという指摘を頂いたので修正しました。また、フラグメントシェーダ内でcolに値が代入されていない場合が存在したので修正しました。(2019/2/24)

VRChatを始めて一年が経ちました

VRChatを始めて一年になったので、この一年を少し振り返りました。おもに、「なにをしたか」という視点でまとめてあります。もちろんほかにもたくさんのことがありました。コミュニティのことがあまり覚えきれなくて書けなかった・・・。

ここから先しばらくは文章というよりは記録という感じの長く、きわめて個人的であり、また乱雑で、コンテクストもある程度要求する文が続きますので、読み飛ばして一番最後の

まとめ

だけ読むことをお勧めします。

 

2018年1月

このとき大学一年生。入学したときサークルにはあんまりいかなくなり、かといって成績がいいわけでもない中、周囲の友人たちが「充実した」大学生活を送っているのを見て超鬱状態で2018年を迎える。そのときに出会ったのがVRChat・・・ではなく、アニメポプテピピック。アニメなどあまり見ない人だったが、ポプテピピックツイッター等で話題になっており面白そうだったので見ることに。これが(個人的に)大変面白く、メンタルが少しずつ回復する。ポプテピピックを見るために1週間を乗り切るような感じになった。そんなこんなで1月下旬になったころ、もともと少しだけ触っていたBlenderで作ったモデルを持ち込むことができる無料ゲームがあると知る。

 

面白そうなので、さのさんのブログを参考にしながらそのゲームに昔作った簡単なモデルを持ち込み、少しそのゲームをやってみることに。これがVRChatとの出会い。当時のVRChatをハックツール全盛期で日本人と会うのが大変難しかった。例えば、日本人がよく集まる、とされていたFantasy Shukaijou はハックツールの流行から閉鎖されており、日本人と会うのが大変難しかった。また、自分にはVRChatをやっている知り合いがいるわけでもなかった。そこで、プレゼンテーションルームを回りながらつたない英語や、日本語のわかる外国人の方との交流を楽しむことにした。ほぼ初めて触るUnityを使ってモデルに簡単なアニメーションを仕込み、エモートで見せて会話の種にしたりしていた(当時はデスクトップモードはアニメーションオーバーライドが使えなかった)。東京グールの主題歌を国もわからない人たちと合唱したのはいい思い出である。そんなこんなで数日過ごしていたころ、ワールド検索でJapanと検索すればいいのでは?と気づいた僕はJapantownに行ってみることに。するとそこには日本語を話す人たちが数人おり、中には日本人もいた。最近始めたものの、もともといたらしいひとたちがハックツールの影響で籠ってしまって、日本人となかなか会えない、という始めたての人たち(要は私と同じ)人たちが多かったように思う。そのひとたちと始めたてのUnityを触ってアバターをカスタマイズして見せ合ったりしながら他愛のない雑談をする、ということをするようになった。この時であった人の一人が元怒さんだったりする。

2月

そんなこんなでのんびりすごしていると、その時あった日本人の一人が「自分のワールド作ったから見に来て」とワールドに案内してくれた。それが大変驚きで、まず「ワールドってユーザが作れるの?????」というセリフで頭の中が埋め尽くされていた。そのときはすさまじく難しい知識がいるのだろうなあと思いながら話を聞いていたが、数日たって、人工衛星を並べた博物館を作ってみたいなあと思うように。(人工衛星が好きだった)

 

調べてみるとねこますさんの動画にたどり着く。ねこますさんのワールド制作動画を見ると大変簡単そうだったので、これなら僕もできるのでは?と思い簡単なワールドを制作。思いのほかうまくできたので、博物館を作っていくことに。途中でやまとさんとひばかりさんと出会い、トリガーの存在と使い方を教えてもらう。最初は全くわからなかったが(文献もやり方もくそもなかったので、お二人がサンプルシーンを読み解いて得た知見を頂いてた)丁寧なご指導により何とか扱えるように。

宇宙博物館を無重力のようにできないものかと考えており、いろいろな方法を試していた。椅子を傾けて写真を撮ると宇宙空間っぽく写真が見えることに気づき、それ関係の調査を行ったりしていた。

 

そのころひばかりさんが、遠見の魔法という遠隔操作魔法をVRChatで実装しようとしており、それにヒントをもらって、無重力システムができないかと考えた結果爆誕したのが空飛ぶ箒である。

 

 

 

これが界隈の中で大変にばずってしまい、テストワールドをあけるといろいろな人が来てくれ、たくさんほめていただいた。何かを作ってたくさんの人に褒めてもらえるという経験がほとんどなかった私にとってこれは大変うれしいことで、またいろいろな改善点を頂いたのでそれをもとに改善をしてみたり、それでまたフィードバックを頂いたりするという活動が始まった。私のほうきの原理を改造して坪倉さんが面白いものを作ったりされており、 

 

 

そういう競争的な何かが発生してそれも大変モチベーションになった。

その高まったモチベーションのまま、自動ドアとか飛行船とかドローンとか、思いつくものをとにかく作りまくったり、

 



日本語のワールドの資料が少なすぎるとトリガーの仕組みの説明動画を「素人が作ったやつでもないよりはある方がましだ」と作ったりした。

 

 

 

この動画はけっこう色々な人に使ってもらったらしく、たまにこの動画でワールド作りを学びました、という話もいただいたりして大変うれしい限り。

ちなみに、このころの技術的競争は恐ろしく激しく、一晩寝たら新しい技術が生まれているような状態だったりし、例えばほうきは私が試作してから2週間たたないうちに小石さんがこのレベルまで昇華させてしまう始末であった

 

3月

3月になっても技術の拡張はどんどん続いていて、いろいろな人がどんどんいろいろなものを作り、それに刺激を受けてほかの人が、という状況が続いた。自分はデスクトップでFPSも10から20しか出ないレッツノートを使っていたのだが、テストワールドを開けるといろいろな人がありがたいことに来てくださり、テストに協力してくださった(VRでどう動くかわからないものをテストしてもらえるのは本当にありがたかった)。おかげであらたなものを作ってフィードバックをして改良する、というサイクルを複数人で行える状況にあり大変楽しかった。また、ほうきの人で終わってはいけない、という強迫観念も自分の中にはあり、モチベになった。

 

実家に帰る時期もあってあまり時間は取れなかったが、それでもいろいろなものを作れた。実家に帰ったときは第一回オンオフ会に参加したりもした。

ちなみに、ほうきがsynqarkさんのtestrunワールドという形で一応完成したのもこの時である。素人の自分の思いつきがここまですごいものになるとは~と圧倒されたし、使ってもらえて本当にうれしかったし、自分の手でアウトプットまで持って行けなかったのが少しだけ悔しかった(笑)。

そして3月下旬、あの人が現れる。

 

 

 

ファイさんのシェーダ芸はシヴァ犬さんに呼ばれて見に行ったのだがとにかくすごかった。シェーダ芸に関してはクロクロさんのパフォーマンスなどで知ってはいたが、ワールドでやってる人はあまりいなかったため、ワールドを作ってた自分は「アバターはすごいなあ」ぐらいにしか思っていなかったが、ファイさんのはワールドにおいてあり(当時の視野狭窄気味の私にはこれが大事だった)、SDKでの「当時」の限界がなんとなくわかっていた私にとっては衝撃的だった。衝撃的すぎて「は~~~」で終わってしまい理解を当時は放棄してしまうのだが。また同時期の同じ時期にhardlightさん(toyboxの作者で海外の方)と話したことも衝撃的だった。自分の作った箒は、synqarkさんのtestrunというかたちでパブリッシュされて海外にも広まったのだが、その時ありがたいことに私の名前も少し広まったらしくその縁で出会うことができた。見せてもらうワールド、ギミック、すべて理解の範疇を超えており「なんでこんなことが?? いままでのJPの技術拡張はなんだったんだ・・・」となってしまうレベルだった。また、なかなかアクセスできない海外技術勢の人たちとの交流の足掛かりができたという点でも大きかった。

4月

4月に入ったころ、バイクのワールドを作ってみたのだが、これがフレンドに意外と好評で、学校が始まる前にやってみるか、と皆さんに協力してもらいながらパブリック化することにした。Just Touring というワールドになる。

 

ここでパブリック化のハードルが低いことを知り、これ以降機会があがればワールドをパブリック化していくことになる。ちなみにこのころの私はリアルタイムライトとベイクという概念すらよくわかってないので当初はリアルタイムでディレクショナルライトで、という感じであった。ただ、これぐらいハードルが低いほうが何もかも初心者の私にはありがたかった。この次に作ったワールドが360mirrot for avatartestとなる。

 

どうやったらアバターの確認がしやすくなるかなあという発想で作ったものである。そして、たぶんこの辺でスタンダードアセットを本格的に使い始める。

そして4月下旬、VIVEをいただくことになる(本当にありがとうございます)。いただく前は、先ほども言った通りVRChatの魅力は作品を自由に作ってフィードバックできることにあると思ってたので、「ピックアップ二つできるようにしたい!」っていう欲望を満たすぐらいしかVIVEに魅力を感じていなかったのだが、ゴーグルをかぶった瞬間にヤバすぎて笑いがこみあげてきたのはよく覚えてる。それ以降しばらくはINするならVIVEという感じになったので、VRってそんなすごいの?って口ではいいながらはまっていたんだろうなあとは思う。事実VR届いてすぐにこういう遊びをしてるし・・

 

5月

今から思えばこの辺がギミックメイカーとしての最盛期だった。「ほうきの人で終わってはいけない」という強迫観念はまだ継続していて、覚えたてのスタンダードアセット使ってギミックを大量生産していた。

電卓とかflappy birdがこのころの作品の代表例

 

 

 

して、そのうち一個がバズる。

 

巨大ロボット操縦システムである。

これはかなりの反響をいただき、Jefclaxさんからモデルを貸していただいてロボットに乗れるワールドを作ったりすることになったり、これの影響でV-TVにも出さしていただいたりした。

 

youtu.be

 

6月

流石に開発スピードが落ちてくる。このころのコンテンツである程度のインパクトを持ったのは多分これだけ 

 

(ちなみに、このワールドはパブリック化したらコミュニティスポットライト入りし、大変うれしかったのを覚えている)

そのかわりといってはなんだが、いろいろなところに出歩くようになった。海外も日本も隔てなくいろいろなところを出歩き、いろいろなワールドを見て回っていたりした。海外コミュニティとの交流は大変新鮮だった。海外の技術勢にアクセスするのは最初は大変なのだが、幸いにも自分はhardlightさんとのつながりがあり、そこからいろいろな人を紹介してもらうことができた。(ほかにも箒の人というので少しだけ名前を知ってもらえたのも幸いした)。時差を考えてインし、拙い英語で何とか意思疎通をしながらワールドを連れまわしてもらう。刺激的な経験だったと思う。ほかにも、ある程度たまった自分のギミックワールドを紹介するツアーなんかを初心者向けにやったりもした。VRChatの可能性を感じてもらえた、ことはあったと思う・・・思いたい。

7月 

実は、5月ごろから雪が降る静かな森を作りたかったし、synqarkさんのワールドは相変わらずあこがれだったし、GPUパーティクルは作りたかった。そう、すなわちシェーダを覚えてエモい表現がしたかったのである。しかしきっかけがなく、プログラミング経験もほとんどなかったので躊躇していた。ところが事件が起こる。6月下旬にcroccaさんがGPUパーティクルを実装したのだ。これまで「ファイさんとneenだけの殿上人の領域」だったGPUパーティクルをほかの人が使ってるのを見て、では勉強してみようとなった。GPUパーティクルを操りたかった。だが、いきなりコードはハードルが高かったのでShaderForgeを始める。最初の一週間はsynqarkさんのVRWaterShaderやストッキングシェーダを真似して、ShaderForgeをどうにか(一応)使えるにした。そしてGPUパーティクルを作ろうとして1週間たって気づく。

 

「これ、ジオメトリシェーダ?とやらが使えないからできないんじゃないか・・・?」

 

そう、GPUパーティクルはShaderForgeでは原理的に不可能だった。そこでファイさんに相談をして、シェーダプログラミングを教わり、手取り足取り指導してもらいながらどうにかGPUパーティクルを完成させる。こうして私の初めてのコーディングのシェーダはGPUパーティクルになった。

 

そしてそのころちょくちょく行くようになってた某所で、いろいろなエフェクトを持ち込んで祭りを使用という企画が立ち上がる。

8月

そしてその某所のイベントがやってくる。私はGPUパーティクルを持ち込んだのだが、とにかく、とにかく、すごかった。花火を打ち上がり、アバターが踊り、音が舞う。とにかく最高の空間で、とくに夜遅く、静かな曲に合わせてGPUパーティクルを降らしたときは忘れられない。そして、覚えたのは強烈な、強烈な、あまりにも強烈な「音に合わせて自分のいま、思ったようにエフェクトを出したいのに、てもとには一定の法則にしたがうGPUパーティクルしかない」という「圧倒的不自由感」だった。この不自由な感覚はとても自分の中に残り、いまだに原動力でもある。

このイベントのために雪を作ったりもした。 

 

ほかのエフェクトをたくさん作ったりした。

 

 

雪は結構いい感じにできてしまい、雪を置いたワールドを作ったら色々な方から評価をいただけた。配布もしたのだが、色々なところで使ってもらえたようでとてもうれしかった。

そしてイベントが終わると、「最高」という感覚と「前回と同じ不自由感」を得た僕の中には音とエフェクトをもっとやってみたいという強い願いが残った。

9月 

前半は学校の海外研修に吹き飛ばされたのでVRChat系は進捗がない。帰ってからは疑似スカイボックスを作ったりしていた。

 

あと、このころ某イベントで使った雪を使ったワールドを量産し始める。あれは正直便利すぎて、とりあえず置いたらエモくなるといった感じだった。

 

10月

桜を作ったりした。

 

 

このころには一種の強迫観念が消えており進捗が非常に緩やかになっていく。某イベントを通じて「バズるもの」ではなく「やりたいこと」を追えるようになった気がする。そして同時に復活した某所に引きこもりを開始する。某所は大変居心地が良く、エモい場所なので気づいたらそこにいる感じなってしまった。

あとは、大学の文化祭で展示するシェーダの制作を行ったりもした。

11月 

平日はシェーダを勉強し、休日は某所に引きこもる、という流れが確立。VRChatのログイン時間がもみるみるへり、代わりに表現を求めてコードとにらめっこする時間が増える。完全にシェーダ沼に落ちた。クリスマスワールドもその一環で出てきた。

 このクリスマスワールドも評価を頂け、配信でも使ってもらったりした。とてもうれしい。

ほかにもレイマーチングを始めたりした。

 

12月 

上旬になんと、おきゅたんに特集してもらうという光栄なことが起きてしまった。3時間近くしゃべらせてもらった。ありがとうございます。

www.youtube.com

 

そしてとある人からお誘いがありリアルタイムエフェクトの追及をようやく始める。某イベントの時に沸き上がった欲求をようやく追求し始めた感じでめちゃめちゃ面白い。

 

毎週要求が加速していき、リアルタイムVRでの表現がようやく少しづつ手を付けられたって感じ。

年末はコミケでVRChatオフ会をするなどした。ここでTeamlabの展示を見に行ったりした。

2019年1月

Teamlabの表現に刺激を受けたので、とりあえずシェーダのみでトレイルを作ってみるなどした。

 

 

 

そのあとは学校の試験があるのでお休み期間にした。生活リズムを直してエモコンテンツを摂取している日々。

 

 

 まとめ

 

私のVRChatライフは始めて1月でギミックがバズる、というかなりのレアケースとして始まりました(そしてインパクト的にはそれを超えられたことはたぶんなかったです(笑))。作ったコンテンツが注目を集めても飽きられるのは一瞬、そう思っていたのでこのチャンスを逃すまいとひたすらに走り続け新しいギミックを考えて実装し続けました。当時のVRChatはギミック的には何もない土地で、その何もない土地をみんなで競って耕してできることを増やしていこう、といういい意味で競争的な空気がありました。素人の僕であっても、何もない土地では何かを作れば最先端であることが、その競争の中にいることができました。おかげで作るコンテンツはそれなりに注目を集めることができました。承認欲求もかなり満たされました(笑)。しかし、時間がたつとそれは限界が出てきます。試験があったりして使える時間が少なくなりました。なにより、でてくるもののクォリティが上がってきました。ところが自分には何もなかった。ShaderもUnityもそしてJavaScriptいじった経験もない人が(当時はwebpanelギミックが流行り始めていてJavaScriptいじれる人が活躍していた)戦える余地なんて発想ぐらいしかなかったのです。そして、いつ立ち消えるか、途絶えるかもわからない「発想」というものに頼って息絶え絶えに何かを作る僕の横で、とある人は時間で、とある人はもともと持っていたスキルですごいものを作り続けていました。VRChatでしんどくなるなんてダサいと思ってたので、しんどいなんて全く思わないようにしてましたけど、たぶん今から振り返ると、しんどかったんだと思います。

そんな僕にとって、GPUパーティクルを作ったことはすごく、すごく大きいことだったんだと思います。技術的には既出、すでにVRChat内にある技術。だけど作れた時の喜び、あこがれていたものがVR空間内で自由に動かせた感動、それはすごいものでした。僕はエモーショナルな風景にずっとあこがれていました。GPUパーティクルを通じて覚えたShaderでエモい雪を作れた時はとてもうれしかった。たぶん、この経験を通じて「作りたいものを作る」という楽しさを少しだけ、ほんの少しだけ知ることができたんだと思います。「バズりたいものを作る」という楽しさ「だけ」でなく(本当にファイさんには感謝しかないです。)。そして技術本位でだけではなく作りたいもの本位でも動けるようになりとても楽しく手を動かせるようになったと思います。

VRChatを始める前はクリエイティブというものとは全く無縁の生活を送っていました。ひたすらにコンテンツを受け取る側で作る側になるなんて考えたこともなかったので、拙いながらも何かを作り続けている日々になったことに自分が一番驚いています。そしてたくさんの人とつながることができました。多くの人が作るものに刺激を受け、何かを作り、フィードバックを受け、そしてまた何かを作る。自分とは縁遠く、選ばれた人たちのものだと思っていたクリエイティブなものに少しだけ触れることができました。また作る側の苦しみ(?)も、本当に少しだけ、少しだけ、知ることができました。前に比べてほんの少し素直に他者の創作物に向き合えるようになったと思います。そうであると信じたいですね。

 

VRChatとVRChatがくれた経験、そしてそれを通じて出会えた本当に素敵な人たちに感謝をしています。これからもよろしくお願いします。