2021の制作物や参加イベント
相変わらずコロナの影響で引きこもり継続な1年でしたが、ワクチンが普及してから徐々にリアルイベント等が開催されるようになってきました。
オミクロン株が広まりだしたのでまた来年以降どうなるかは未知数ですが、またリアルクラブで爆音と振動と光を浴びれるようになるといいなぁと思います。
ということで、今年の振り返りをしました。
記憶力が皆無なので、自分でもこんなことしてたのか!?と新鮮な驚きと発見がありました。
VJ
1/29 GHOSTCLUBE TEXTURETYPE:CONCRETE
CONCRETE専用のコンクリ張りのめっちゃ渋いワールドでのイベントでVJしました。
「CONCRETE(コンクリート)」というコンセプトに沿った映像を作りました。
リアルタイムGIでモニタの映像で空間がビカビカするのは、現実のクラブのようで気持ちよかったです。
今回のテーマが「CONCRETE」だった。CONCRETEには「具体的な、具象の」という意味もあるので、今回は抽象的なパーティクルや光り物は避けて質感にこだわることにした。 #TEXTURETYPE pic.twitter.com/agN16tSJuk
— kaiware style🌱メタバースVJ (@kaiware007) 2021年2月1日
6/18 GHOSTCLUBE CONCRETE2
ワールドがGHOSTCLUB 5.0になり、CRTモニタやサイドランプをVR側で操作できるようになりました。
無理を言って外部から制御できるようにしてもらい、UnityからVRChatのCRTやサイドランプを制御しました(fotflaさん、phiさんありがとう🙏)
5x5のCRTにBozeを映せたので大満足です。
#CONCRETE2 #everyday1boze pic.twitter.com/MMkG1UKi8J
— kaiware style🌱メタバースVJ (@kaiware007) 2021年6月19日
7/27 DOMMUNE
縁あって憧れのDOMMUNEでVJすることができました。
久しぶりに現場で爆音を浴びれたので、リアルクラブが懐かしくなりました。
DOMMUNEの宇川さんにサイケ背景のVJシーンを気に入ってもらえて、最後までずっと流し続けてもらえました。
お疲れ様でした! pic.twitter.com/1lRfdPa4Yv
— kaiware style🌱メタバースVJ (@kaiware007) 2021年7月27日
8/27 GHOSTCLUBE Weekly Event
CONCRETE2の頃のシステムをそのまま使用。
12/11 12/12 サンリオバーチャルフェス
まさかのサンリオバーチャルフェスにVJとして参加できました(0b4k3さん本当にありがとう!🙏)
縦画面という特殊な映像システムに対応するため、解像度と画面の向きを変更できるように改修。
さらにDJのアバターを使ってゲームっぽい演出をしたかったので、VRMをランタイムに読み込めるようにしました。
ゲームっぽい演出を作るためには、ゲームっぽい挙動をするシステムを作る必要があるんですが、当たり判定や敵の出現システム、キャラクター制御など、普通に演出作るよりも手間がかかりました…。
また、タッチディスプレイを導入して操作を直感的にしました。
サンリオVフェスありがとうございました!今回自作UnityVJシステムをVRM対応して、実行時にDJのアバターを取り込んで演出に使えるようにしました!こういうのができるのはゲームエンジンならではって感じですね。楽しくVJできました! #sanrio_vfes #ALT3 #madewithunity #vj pic.twitter.com/DHMw6qvkjD
— kaiware style🌱メタバースVJ (@kaiware007) 2021年12月14日
12/24 GHOSTCLUBE Xmas Event
サンリオバーチャルフェスのシステムを流用。
ShaderFes 2021
12/17からオープンしたShaderFes2021に、3作品出展しました。
Boothの新作もないので、正直出そうかどうか迷ったんですが、本業でもなかなかシェーダーを書く機会がなかったのと、せっかくだしBozeの知名度をあげようと思い参加しました。
R01、B07、Alpha展示室で展示しています。
ShaderFesオープンしました!自分は、R01、B07、Alpha展示室で展示しています!https://t.co/3bUyBYKmzi #ShaderFes #VRChat
— kaiware style🌱メタバースVJ (@kaiware007) 2021年12月17日
Bozone
#everyday1boze pic.twitter.com/KR8D68L4Lz
— kaiware style🌱メタバースVJ (@kaiware007) 2021年11月3日
とにかくきれいなBozeを作ってみたいと思い作成しました。
三日坊主を止めるな2の演出で作ったパーティクルBozeを元に、大量かつ軽量をめざして調整しました。
ShaderFes2021 Alpha展示室の「A3 Bozone」は、自前のMeshTopology.PointsのMeshの頂点を使ってBozeをレンダリングしてます。今回、通路と中央広場の部分はBozeが出ないように、Editor上でコライダーのある部分に頂点を作らないようにPointMeshを作る拡張Editorを作りました。 #ShaderFes #shader pic.twitter.com/jLE46LzMbc
— kaiware style🌱メタバースVJ (@kaiware007) 2021年12月19日
スライダーで3種類のモードを切り替えれるようにしてます。
Colorful Bozeモード
カラフルなパーティクルBozeがふわふわ漂うモード。
Bozeの環境ライトをONにすることで、Bozeの質感や表情が見えるようになります。
「チー○ラボみたい」という感想があったらしく、思惑通りにいってニヤニヤしてしまいました。
Boze Structureモード
パーティクルBozeがくびれた円柱状に配置されてぐるぐる回るモード。
Twitterでも割とこのモードの写真を見ることが多かった気がします。Snow Bozeモード
パーティクルBozeが雪のように上から下に降り注ぐモード。
スライダーで降る速度を変更できます。
MetaBoze
最初に生まれたBoze、原初のBozeです。
フラクタル状のキモい生命体、かわいい、怖い、トラウマ、3回見たら死ぬ絵、等色々言われて面白かったです。
MirageLady
前回のShaderFesでは、GlitchBoyを出展したので、今回はMirageLadyを出展しました。
(それでも一番売れてるのはGlitchBoyだったりする)
Crypto Art Fes
今年はNFTが大きく盛り上がりましたが、自分もBozeをいくつかNFTにしてみました。 その中で、CryptoArtFesというイベントにいくつか出展させていただきました。
#CryptoArtFes pic.twitter.com/2gb4IVQtSC
— kaiware style🌱メタバースVJ (@kaiware007) 2021年4月10日
Bozeたち
ShaderToy や Shader 1 Week Compoで新作Bozeをいくつか作りました。
昨年に比べると圧倒的に少ないですが、昨年のモチベが異常過ぎました。
Boze Tシャツ
Boze T-Shirt Series Now on sale!https://t.co/eLJvFFS4Yjhttps://t.co/TtdmCoVppD pic.twitter.com/2tmNNkpGod
— kaiware style🌱メタバースVJ (@kaiware007) 2021年5月20日
リアルBozeグッズを作りました(三日坊主さんには許可は取ってます)
なお全く売れてない模様。
まとめ
自作VJシステムも成熟してきた気がしますが、機能の増加にUIの整理が追いつかない感じになってきたので、来年はUIのブラッシュアップをしていきたいです。
あとは、なんだかんだで、相変わらずBozeを作ったりしてましたね。 世界で唯一のBoze職人かもしれません。後継者募集中です。
ついに現実空間に進出し始めたBozeと、サンリオ中途入社IPフラグがたった三日坊主さん、来年も目が離せませんね!
来年もよろしくおねがいします。
Boze入門
この記事はBoze Advent Calendar 2020の2日目の記事です(遅くなって申し訳ありません)
https://qiita.com/advent-calendar/2020/boze2020
前日は、オリジナルの三日坊主さんの「Bozeって何?」でした。
とても哲学的で良い記事でしたね。
前日の記事で"Boze"が何かわかったと思いますので、今回はより実践的な"Boze"について書いてみたいと思います。
unity 進歩tiktok できた〜です〜✨✨✨
— 三日坊主✨ (@mikkabouzu777) 2018年7月5日
アニメーションのキー操作でけたでけた〜です〜✨✨
あと、スクリプトも初めて弄ったです…✨✨難しいです…✨✨
バッターパーティーピーポ〜✨✨✨(夏っぽい🌞) pic.twitter.com/Tycjm3aXoC
Boze入門
私の"Boze"は、レイマーチングを用いた"Boze"です。
早速ですが、最低限のレイマーチングによる"Boze"のサンプルです。
neort.io
最低限ですが、結構な長さがあるので、要点部分だけ抜粋して解説します。
レイマーチングとは
レイマーチングとは、その名の通り、レイ(光)をマーチング(行進)させてレンダリングする手法です。
↑親の顔より見たレイマーチングの図
レイは、各図形の距離関数からレイとの距離を計算し、その距離の長さだけレイを進めて、再度距離を求めて…というのを繰り返していく感じです。
距離がものすごく小さくなったら図形に接触していると判断します。
ソース上では、traceRayがそれにあたります。
// 距離関数をマッピングする処理(空間の中心にBozeがあるだけ) surface map(vec3 p) { surface result = SURF_NOHIT(1e5); float ms = sin(iTime) * 0.5 + 0.5; // boze surface boze = sdBoze(p, vec3(1), ms); result = opU(result, boze); return result; } // レイを進ませる処理 surface traceRay(in vec3 origin, in vec3 direction, float dist, out vec3 pos) { float t = 0.0; pos = origin; int count = 0; surface hit; float d; for (int i = 0; i < MAX_MARCH; i++) { hit = map(pos); d = hit.dist; if (d <= EPS || d >= MAX_DIST) { break; } t += d; pos = origin + direction * t; count++; } hit.dist = t; hit.count = count; pos = origin + direction * t; if(hit.isHit) { // Lighting vec3 normal = norm(pos); vec3 lightDir = normalize(vec3(cos(iTime), 1, sin(iTime))); vec3 lightColor = vec3(1.5); float NoL = saturate(dot(normal, lightDir)); vec3 ambientColor = vec3(0.1); hit.albedo.rgb *= NoL * lightColor + ambientColor; } if(d <= EPS){ hit.isHit = true; return hit; }else{ hit.isHit = false; return hit; } } // レイを進ませて、図形と衝突してなかったら背景色にする処理 vec3 render(vec3 p, vec3 ray, vec2 uv) { vec3 pos; surface mat = traceRay(p, ray, 0., pos); vec3 col = vec3(0,0,0); vec3 sky = vec3(0.3); col = mat.isHit ? mat.albedo.rgb : sky; return col; }
レイが図形に接触したら、図形の表面の色を取得する必要がありますが、"Boze"の場合、肌、目、口とそれぞれ色が違います。
そのため、各図形から距離を取得するときに、一緒に色も返すように戻り値を構造体にしています。
// 距離関数からの情報を格納する構造体 struct surface { float dist; // 距離 vec4 albedo; // 色 int count; // 進んだ回数 bool isHit; // 衝突フラグ };
Bozeの構成要素
"Boze"は、基本的な距離関数の組み合わせでできています。
下記に"Boze"で使っている基本的な距離関数一覧を紹介します。
sdCappedTorus:欠けたトーラス(リング)
float sdCappedTorus(in vec3 p, in vec2 sc, in float ra, in float rb) { p.x = abs(p.x); float k = (sc.y*p.x>sc.x*p.y) ? dot(p.xy,sc) : length(p.xy); return sqrt( dot(p,p) + ra*ra - 2.0*ra*k ) - rb; }
sdEllipsoid:楕円球
float sdEllipsoid( vec3 p, vec3 r ) { float k0 = length(p/r); float k1 = length(p/(r*r)); return k0*(k0-1.0)/k1; }
sdCapsule:カプセル
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) { vec3 pa = p - a, ba = b - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return length(pa - ba*h) - r; }
sdRoundBox:角丸立方体
float sdRoundBox(vec3 p, vec3 size, float r) { return length(max(abs(p) - size * 0.5, 0.0)) - r; }
sdRoundedCylinder:角丸円柱
float sdRoundedCylinder( vec3 p, float ra, float rb, float h ) { vec2 d = vec2( length(p.xz)-2.0*ra+rb, abs(p.y) - h ); return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rb; }
画像出典: distance functions by iq www.iquilezles.org
"Boze"の各パーツ
頭部
なめらかな凸型の頭部は、カプセルと角丸円柱を融合させて作っています。 上部がカプセルで、下部が角丸円柱です。
// head float d = sdCapsule(p, vec3(0,0.05,0), vec3(0, 0.11, 0), 0.125); float d1 = sdRoundedCylinder(p + vec3(0,0.025,0), 0.095, 0.05, 0.0); d = smin(d, d1, 0.1);
2つの図形をSmooth minというなめらかに補完する関数を使って融合させています。
float smin( float d1, float d2, float k ) { float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 ); return mix( d2, d1, h ) - k*h*(1.0-h); }
耳
耳は、欠けたトーラスを90度ほど傾けて作っています。
float sdEar(vec3 p) { p = rotate(p, RAD90+0.25, vec3(0,0,1)); return sdCappedTorus(p + vec3(0.05, 0.175, 0), vec2(sin(0.7),cos(0.7)), 0.03, 0.01); }
また、顔のパーツは口以外は左右対称なので、レイの座標をX座標を絶対値にすることで、空間が左右対称になって距離関数を削減しています。
vec3 mxp = vec3(-abs(p.x), p.yz); // ear float d2 = sdEar(mxp); d = opU(d, d2);
口
口は楕円球を使っていますが、口角を若干上げるために楕円球の中心からX方向に離れるほど回転させて歪めています。
float sdMouse(vec3 p, float ms) { vec3 q = opBendXY(p, 2.0); ms += 0.00001; return sdEllipsoid(q - vec3(0,0,0.2), vec3(0.035, 0.01 * ms,0.05 * ms)); } vec3 opBendXY(vec3 p, float k) { float c = cos(k*p.x); float s = sin(k*p.x); mat2 m = mat2(c,-s,s,c); return vec3(m*p.xy,p.z); }
目と眉毛
目は見たまんまですがカプセル、眉毛は角丸をすごく細くして作っています。
float sdEyeBrow(vec3 p) { const float x = 0.05; p = opBendXZ(p + vec3(0.02,0,-0.02), -6.5); return sdRoundBox(p + vec3(0.005, -0.14,-0.11), vec3(0.003,0.0025,0.05), 0.001); }
// eye float d4 = sdCapsule(mxp, vec3(-EYE_SPACE, 0.06, 0.13), vec3(-EYE_SPACE, 0.08, 0.125), 0.0175); surface eye = SURF_BLACK(d4); // eyebrows float d9 = sdEyeBrow(mxp); eye.dist = opU(eye.dist, d9);
頬
パーツ単位で一番複雑なのが、頬のうねうねの線です。
元のデザインでは手書きでうねうねさせていた線ですが、レイマーチングでは3本のカプセルを歪めながら連結して再現しています。
float sdCheep(vec3 p) { const float x = 0.05; const float z = -0.175; const float r = 0.0045; const float rb1 = 100.; p = rotate(p, M_PI * -0.6 * (p.x - x), vec3(-0.2,0.8,0)); float d = sdCapsule(opBendXY(p + vec3(x, -0.01, z), rb1), vec3(-0.005,0.0,0.0), vec3(0.005, 0., 0.001), r); float d1 = sdCapsule(opBendXY(p + vec3(x+0.01, -0.01, z), 200.0), vec3(-0.0026,0.0,0), vec3(0.0026, 0., 0), r); float d2 = sdCapsule(opBendXY(p + vec3(x+0.019, -0.015, z), -rb1), vec3(-0.01,0.0,-0.01), vec3(0.0045, 0., 0.0), r); return opU(opU(d, d1), d2); }
パーツの合成
前述の顔のパーツを合成します。
距離関数での図形の合成は、以下のとおりです。
// 和 float opUnion( float d1, float d2 ) { min(d1,d2); } // 除 float opSubtraction( float d1, float d2 ) { return max(-d1,d2); } // 交差(論理積) float opIntersection( float d1, float d2 ) { return max(d1,d2); }
まず、顔と耳と頬をopUnionで合成します。
目、口、眉毛は、凹んでいるのでopSubtractionで削除します。
これで完成!
君だけの"Boze"を作ろう!
2020年上半期で作ったRaymarchingまとめ
正月
Cyber Fuji 2020
VRChatの一部界隈でお正月の振り袖の模様をシェーダーで作るムーブメントがあったので便乗して作ってみた作品です。
みんなだいたい和風で作ってたので、ハマってたvaporwaveというかsynthwave的な雰囲気のものを作ってみました。
VRChatに持っていったバージョンは、日が登ったり沈んだりする動きも追加しています。
バーチャルとはいえ、衣服の模様がアニメーションするのはインパクトがあって反応を見るのが楽しいです。
Boze ノ ハジマリ
ある日、天啓を得て、VRChat界のNo1ゆるキャラ(?)、三日坊主さんを距離関数してみました。
これが後の"Boze"シリーズとしてそこそこ続くとは、このときは全然考えていませんでした…。
Distance Function : Mikka Bouzu
基本の三日坊主さんの顔の距離関数です。
通称、Boze。
基本的な距離関数を組み合わせて作っています。
00 Meta Boze
距離関数にすると何が便利かと言うと、モーフィングさせたり、ねじれさせたり、融合させたりと普段ポリゴンベースのリアルタイムレンダリングだと難しい処理もできるようになることです。
とはいえ、そもそもレイマーチング自体が重いので万能ではないですが…。
この頃は、まだ最適化が甘くてめちゃくちゃ重いです。
あまりに重すぎたので、Neortには動画にして投稿しています。
3月にネット上で開催されたShader1weekCompoというイベントに出展しました。
1週間という限られた時間の中でシェーダーを使った作品を作るというニッチなイベントでしたが、割と作りたいものがはっきりしていたのですんなりと完成できました。
まぁテーマは完全無視してましたが…。
進撃のBoze
01 Meta Boze Again
5月に開催された第2回のShader1weekCompo用の作品です。
テーマは「続く」ということで、前回のMetaBozeさんを続投させるということで無理矢理合わせました。
無限にBozeがうねうね伸びていくように見せれて満足です。
Everyday 1 Boze
第2回のShader1weekCompoが終わってから、もっとレンダリングの勉強やレイマーチングのテクニックを身につけたいなと思い立ち、色々Bozeを使って模索をはじめました。
ネタがポンポン思いついたので、毎日投稿できるかもしれないと思って #everyday1boze というタグをつけて投稿し始めました。
結局毎日は無理でしたが…!
02 Party Boze
グリッドごとに違う動きや色をだせるか試してみた作品その1。
空間をmod関数などでグリッドで区切ってリピートさせてオブジェクトを増やす手法は、レイマーチングではよくやるのですが、グリッドごとに乱数を使ったりすると、形が崩れる問題が度々おきます。
原因は、グリッド内での距離の分だけレイを進ませた時に、グリッドの境界を突き抜けて次のグリッドに大幅に食い込んだりするためです。
このときは、単純にレイが進む距離を半分にしてはみ出しづらくしてごまかしています。
03 Darkmatter Boze
グリッドごとに違う動きや色をだせるか試してみた作品その2。
モチーフは、子供の頃に図鑑か何かで見た「宇宙誕生以前の図」、磁性流体、Max CooperのOriginsのMVです。
04 Endless Boze Tunnel
自分としては、ずっと見ていられるようなものを作りたいモチベーションがあります。
しかし、何故か無限にループするようなトンネル系のものを作ったことがなかったので作ってみました。
これのためだけに、Boze関数の口の中を深くしました。
実際は拡大してるだけなんですが、中に入っていくような感じに見えて面白かったです。
05 Drop Boze feat. Daruma-Otoshi
空間のグリッド化をしつつ、破綻しないように頑張ろうとしたけどうまく行かなかった作品。
1つの顔をグリッドで分割して、グリッドごとの顔の位置をずらしたら、達磨落としみたいなものができそうと思って作りました。
結局このときは、グリッドではできなくて、5つの顔を段々になっているBoxとの論理積をとって残った部分をずらしている感じで表現しています。
1つのBoze関数でもそこそこ重いのを、5個も描画してるのでかなり重いです。閲覧注意!
06 Ghost Boze
空間のグリッド化は、一旦諦めて、ボリュームレンダリングに挑戦してみた作品。
凹みさんのボリュームレンダリング記事がめちゃくちゃ参考になりました。
この世のUnityについての面白そうな技術は、だいたいkeijiro神か凹み神が既にやってたりします。
我々人類は、ありがたく神の恵みを享受しましょう。
通常のレイマーチングで床と天井を作ったあとに、Depthを保持して、ボリュームレンダリングをしているので、うまく床や天井にめり込むようになってます。
07 Cloud Boze
ボリュームレンダリング作品その2。
FBMの値に距離関数の距離を引くことで、雲が形作られてます。
図形との距離が遠いと濃度が0になり、中にはいると距離が負になるので濃度が濃くなる寸法です。
シンプルだけど面白いですね~。
08 Grid Dissolve Boze
やっと空間のグリッド分割がうまくいった作品。
レイを進ませる時に、必ずグリッドの境界で一旦止まるようにしています。
距離関数のグリッドの立方体の表面との距離と、距離関数の戻り値の距離を比較して、短い方を採用しています。
グリッド境界で必ず止まるので、普通にレイマーチングするよりは試行回数が増えてしまいます。
09 Round Slice Boze
空間分割作品その2。
グリッド自体は立方体ですが、ずらし方を段ごとに同じにしてるので、輪切りされてるように見せてます。
断面にノイズによるサイケな模様をつけたり、背景をピンクにしたりして怪しい雰囲気を出してみました。
10 BRDF Boze
本格的にBRDFについて勉強しはじめた作品。
リアルタイム鏡面反射たのし~!
まだ反射処理を1回分しか行っていないので、床に写り込んでいるBozeの表面が反射していないです。
11 U.F.B(Unidentified Flying Boze)
寝る前に、「カプセルの距離関数は、内部的に直線上の点の位置を計算できてるから、その点上に別の距離関数を置いて距離を返したら、引き伸ばされた図形ができないか?」と思いついて作ってみた作品。 割と思ったとおりの感じの絵になってよかったです。
12 Color Blend Boze
色や質感をなめらかに合成してみたくて作った作品。
今まで、質感の設定は、衝突した距離関数に応じてインデックスを決めて、最後にまとめて色を割り振るみたいなことをしていましたが、それだとsmoothminなどで複数の物体がブレンドされていても境界付近がぱっきりわかれてしまっていました。
今回は、距離関数に色などの質感設定も返すようにしてsmoothminでもなめらかにブレンドされるようにしてみました。
結果、switch~case文が減ってシンプルになった面もありますが、距離関数を書くのが若干めんどくさくなりました…。
質感や色味は割と気に入っています。
13 Boze Factory
複数回の鏡面反射とエミッション(発光物)をやってみた作品。
エミッションは単純に足せばいいだけなので簡単ですね。
ただ、レイマーチングでブルーム処理は難しいのでまだできていません(2パス使えれば簡単にできそう)
複数回の鏡面反射は、反射物に写った自身の表面にも、更に反射した絵が写ってないと不自然だったのでやってみたって感じです。
反射の計算が1回だけだと、反射物の表面に写った物体の反射計算が行われません。
単純に試行回数増やすだけなので重いけど、その分情報量もマシマシになるので楽しかったです。
14 Crystal Boze
レイマーチングでReflect(反射)をやったので、Reflact(屈折)もやってみたいな~と思って作った作品。
球体は透過してるように見えるけど、実際はほぼ全反射する物体です。背景のせいでわかりにくいですね。
レイマーチングの屈折表現はがむさんのシェーダーを参考に実装してみました。
物体内部にレイを進ませるために距離を絶対値にしたり、法線と進行方向の内積を取ることで内外判定したり、と色々勉強になりました。
はじめ、屈折の法則を知らなかったので、なんで屈折率の逆数を使うのかわからなかったんですが、高校物理のサイトとか見て外に出る時は屈折率が入れ替わるということを知って理解できました。
レイは形状に応じて屈折するので、違和感が無くなるようにBozeの眉毛の深さなど細かいところを調整しました。
15 Non-uniform Scaling Boze
大きさの指定がXYZ軸ごとに指定できるようにしたBozeのテスト。
座標を大きさで割ったら、図形の大きさも変えれるのは知ってましたが、アーティファクトが出たりしちゃってうまく行かなかったんですが、最後にXYZ軸で最も小さいサイズをかけるとうまくいくことを知りました。
床の模様のせいもあるが「カー○ィボール」と言われました。
まとめ
正月のCyberFuji2020以外、全部Bozeやんけ!
無機質になりがちなレイマーチング作品ですが、愛嬌のある三日坊主さんの顔を使うことで、キャラクター性が出て楽しく見えてモチベーションが上がってます(いつも広い心で許していただいてありがとうございます)
作品作りを通して、自前のレイマーチング用のレンダリングエンジン(?)が出来つつあるので、TDFやRevisionなど、シェーダー系のイベントに作品を投稿してみたいな~という気持ちになりつつあります。
今年ももう半分過ぎてしまいましたが、これからもBozeを作り続けたいです。
また、現在、趣味でjavascript(というかWeb開発全般)とWebGLの勉強がてら、タイムライン操作できるShaderToyもどきを作っています。
自分が欲しいからという理由で作ってるので公開するかは未定です(完成するかも未定)
CSSわからん…なんも…
さいごに
twitterにもboze作品の動画を投稿してますが、ほぼ全て避雷さんのShaderRecというサイトでキャプチャしています(いつもお世話になっております) shader-rec.herokuapp.com
【VRChat】GPUパーティクル同士が線で結ばれるワールド「Simple Plexus」の解説
前置き
本記事は、VRChatアドベントカレンダー2019の2日目の記事です。
前日はほたてねこまじんさんの「自分の部屋をデジタル空間に持っていった話」でした。
今回は、VRChatで自作したワールドの簡単な紹介と、そのワールドで全体的に使用している自作GPUパーティクルの実装の解説を書きたいと思います。
ワールド概要
記事のトップの画像が今回作成した「Simple Plexus」ワールドのスクリーンショットです。
空間全体にGPUパーティクルを敷き詰め、パーティクル同士が近づくと線で結ばれる仕組みになってます。
いわゆるAfterEffectの「Plexus」プラグインをシェーダーで一部再現してみた感じです。
ワールドのリンクは下記にあるのでVRChatユーザーはぜひ来てください。 vrchat.com
ワールドの中央には、ローポリでツルピカな大仏が鎮座して周囲のパーティクルの光を反射しています。
モデル自体は、Yahoo! JapanがCreative Commonsライセンスで配布している大仏の3DデータをBlenderで頂点数を削減してカクカクにしたものを使用しています。
モデルのシェーダー自体はUnity標準のStandardShaderですが、ReflectionProbeで毎フレームCubeMapを生成してパーティクルの明滅を反映させてます。
大仏の右の方には、パーティクルの制御用のコントロールパネルがあります。
左から、
- Wave
パーティクルは、一定周期で大きさが変わったり線が繋がらなくなったりですが、その変化の度合いの調整をするパラメータです。0にすると明るさがほぼ変わらなくなって線もつながる条件ならつながり続けます。 - Time Speed
パーティクルの動く速さのスライダーです。0にすると止まります。 - Connect Distance
パーティクル同士を線で結ぶ距離のしきい値のスライダーです。パーティクル同士の距離がしきい値以下になると線で結ばれます。簡単に言うとスライダーを上げると線が結ばれやすくなって、下げると結ばれにくくなります。 - Reset Parameter
上記3つのパラメータを初期値にリセットするボタンです。 - Mirror
鏡を出したり消したりするボタンです(撮影用)
これらのパラメータを変更することでワールドの雰囲気を多少変化させることができます。
Connect Distanceを最大まで上げた状態↓
実装解説
ワールドの紹介はここまでにして、実装の解説に移ります。
VRChatは、ユーザーが自作したアバターやワールドをアップロードすることができますが、ユーザーが自作したC#スクリプトやコンポーネントは使用できないという制約があります。
ただし、自作シェーダーは使えるので、工夫次第でいろんな事ができます。
「Simple Plexus」ワールドも、パーティクル部分の仕組みはすべてシェーダーで実現しています。
今回サンプルとしてGPUパーティクルの部分だけを取り出したプロジェクトをgithubで公開します。
簡略化のために、スライダーによるシェーダーパラメータの変更を抜いているので、実際にVRChatで公開してるワールドとは若干仕様が違います。
CustomRenderTextureについて
今回、UnityのCustomRenderTextureという機能を使ってパーティクルの座標更新を行いました。 CustomRenderTextureの詳しい仕様についてはUnityのマニュアルか、凹みさんのブログを参照してください(いつも大変お世話になっております)
ものすごく簡単に説明すると、シェーダーつきのテクスチャです。毎フレームシェーダーを走らせて絵を変えることができます。
今回の自分の使い方としては、パーティクルの更新した座標を格納するのに使っています。
まず、CustomRenderTextureの設定ですが、32x32x32頂点の立方体メッシュの一頂点をCustomRenderTextureの1ピクセルとして扱いたかったので、32x32x32の解像度の3次元テクスチャにしています。
パーティクルの素になるメッシュの作成
VRChatはC#スクリプトが使えないので、Graphics.DrawProceduralなどの直接的に描画処理を呼ぶAPIが使えません。
そのため、通常のMeshRenderにMeshを登録して描画させなければなりません。
しかし、32x32x32頂点の立方体をBlenderで作るのも(操作に慣れてないのもあり)面倒なので、一辺をN頂点で分割した立方体状の点群メッシュを生成するEditor拡張を作りました。
32x32x32回分Vertexシェーダーが走ればいいだけなので、必要分の頂点座標の配列と、ただ順番に番号を詰め込んだIndex配列だけセットしています。
パーティクルや線のポリゴンはGeometryシェーダー内で生成しているので、UV座標もセットしていません。
注目ポイントは、Mesh.SetIndicesの2番めの引数でMeshTopology.Pointsを指定して点群メッシュとして生成しているところです。
パーティクルの座標更新
前項でMeshを作りましたが、実際の描画にはMeshの頂点座標の値は使いません。 頂点座標は、CustomRenderTextureの中で計算して書き込み、描画時にそれを取り出して使います。
実際にCustomRenderTextureの更新に使っているシェーダー(PlexusUpdate.shader)のメインの部分は下記になります。
float4 fragUpdatePosition(v2f_customrendertexture i) : SV_Target { // テクスチャのUV座標(3次元) float3 pos = i.globalTexcoord; // 1ドット1メートルとする pos *= float3(_CustomRenderTextureWidth, _CustomRenderTextureHeight, _CustomRenderTextureDepth); // スライダーから時間の速さを取得 float3 sliderPos = readDataPos(1); float range = _MaxY - _MinY; float limitY = clamp(sliderPos.y, _MinY, _MaxY); float normalizeY = saturate((limitY - _MinY) / range); float timeSpeed = lerp(_TimeSpeedMin, _TimeSpeedMax, normalizeY); // RTX2080系だとノイズ関数で浮動小数点演算の誤差かなんかで結果が偏りまくるのであまり大きな値にならないようにしている(ループするときに微妙だけど…) float time = fmod(_Time.y * timeSpeed, 256) + 138.21; // シンプレックスノイズで座標をゆらゆらさせてる float noiseSpeed = snoise(float2(pos.x + pos.y * _CustomRenderTextureWidth + pos.z * _CustomRenderTextureWidth * _CustomRenderTextureHeight / 34.2148, time * _PositionSpeed1 + 32.153)); float yz = time * noiseSpeed * _PositionSpeed2; pos.xyz += snoise3D(pos.xyz * _PositionNoiseScale + float3(yz * 0.1, yz * 0.34, yz * 0.75)) * _PositionRange; pos /= float3(_CustomRenderTextureWidth, _CustomRenderTextureHeight, _CustomRenderTextureDepth); // 正規化する // テクスチャに座標を書き込む return float4(pos, 1); }
3次元テクスチャのピクセル数分fragUpdatePositionが実行されます。
3次元テクスチャの1ピクセルを1頂点として、その頂点の座標と時間をもとにノイズでゆらゆらさせた座標を最後にRGBチャンネルにXYZ座標を書き込んでます。
明らかに1を超えたり負の値になったりしますが、テクスチャのColor Formatの設定をRGBA Floatにしているので問題ありません。
パーティクルの描画
描画は、PlexusDraw.shaderで行っています。
シェーダーの中身は、主にVertexシェーダーとGeometryシェーダーとFragmentシェーダーに分かれています。
Vertexシェーダーから見ていきましょう。
struct appdata { float4 vertex : POSITION; uint vid : SV_VertexID; }; struct v2g { float4 vertex : SV_POSITION; uint vid : TEXCOORD0; float scale : TEXCOORD1; float particleIntensity : TEXCOORD2; float lineIntensity : TEXCOORD3; float4 positions[13] : TEXCOORD4; }; // 省略~~~~ sampler2D _MainTex; float4 _MainTex_ST; UNITY_DECLARE_TEX3D(_PosTex3D); float3 _PosTexTexelSize; float _Size; float _SizeNoiseScale; float _SizeNoiseSpeed; float _LineWidth; float _Intensity; float _FogPower; float4 _Color; float3 _ColorNoiseScale; float _ColorNoiseSpeed; float _ColorSat; float _ConectDist; v2g vert(appdata v) { v2g o; float3 vpos = v.vertex.xyz + _PosTexTexelSize.xyz * 0.5; // 自身のUV座標 // 3次元テクスチャから頂点座標を取り出す float4 pos = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos, 0); float time = fmod(_Time.y, 256) + 132.5347; // 自身の頂点座標をワールド座標に変換 o.vertex = mul(unity_ObjectToWorld, pos); o.vid = v.vid; o.scale = clamp((_Size + (snoise(float2((float)v.vid / 234.2148, time * _SizeNoiseSpeed + 32.153)) * 0.5 + 0.5) *_SizeNoiseScale), 0.01, 1); // パーティクルや線のの明るさ計算 o.particleIntensity = saturate(exp(-distance(o.vertex.xyz, _WorldSpaceCameraPos.xyz) * _FogPower)); o.lineIntensity = saturate(o.particleIntensity); // 隣接する頂点の座標を取得 float4 pos0 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(0, 0, _PosTexTexelSize.z), 0); float4 pos1 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.x, 0, _PosTexTexelSize.z), 0); float4 pos2 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.x, 0, 0), 0); float4 pos3 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(0, _PosTexTexelSize.y, 0), 0); float4 pos4 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(0, _PosTexTexelSize.y, _PosTexTexelSize.z), 0); float4 pos5 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.x, _PosTexTexelSize.y, _PosTexTexelSize.z), 0); float4 pos6 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.x, _PosTexTexelSize.y, 0), 0); float4 pos7 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.x, -_PosTexTexelSize.y, _PosTexTexelSize.z), 0); float4 pos8 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.x, -_PosTexTexelSize.y, 0), 0); float4 pos9 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.x, -_PosTexTexelSize.y, _PosTexTexelSize.z), 0); float4 pos10 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.x, 0, -_PosTexTexelSize.z), 0); float4 pos11 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(0, _PosTexTexelSize.y, -_PosTexTexelSize.z), 0); float4 pos12 = UNITY_SAMPLE_TEX3D_LOD(_PosTex3D, vpos + float3(_PosTexTexelSize.xy, -_PosTexTexelSize.z), 0); // ワールド座標に変換 o.positions[0] = mul(unity_ObjectToWorld, pos0); o.positions[1] = mul(unity_ObjectToWorld, pos1); o.positions[2] = mul(unity_ObjectToWorld, pos2); o.positions[3] = mul(unity_ObjectToWorld, pos3); o.positions[4] = mul(unity_ObjectToWorld, pos4); o.positions[5] = mul(unity_ObjectToWorld, pos5); o.positions[6] = mul(unity_ObjectToWorld, pos6); o.positions[7] = mul(unity_ObjectToWorld, pos7); o.positions[8] = mul(unity_ObjectToWorld, pos8); o.positions[9] = mul(unity_ObjectToWorld, pos9); o.positions[10] = mul(unity_ObjectToWorld, pos10); o.positions[11] = mul(unity_ObjectToWorld, pos11); o.positions[12] = mul(unity_ObjectToWorld, pos12); return o; }
元のメッシュの頂点座標を3次元のUV座標に変換して、3次元テクスチャからUNITY_SAMPLE_TEX3D_LODで頂点座標の情報を取り出しています。
また、次のGeometryシェーダーで隣接する頂点と線を結ぶために、隣接する頂点の座標も配列にセットしています。
3次元空間をグリッド状に区切り、1グリッドの中に1つの頂点が入ってると仮定します。
その場合、隣接するグリッドは全部で26個1あります。流石にすべてのグリッドが26個ずつ調べるのは負荷が大きそうです。
しかし、線は点と点を結ぶものなので、線がつながるかどうかは片方の点(グリッド)が判定するだけでいいのです。
つまり、すべての点(グリッド)が自分の右下方向だけを見るようにすれば、各グリッドが判定する隣接グリッド数は7個になります。
1. (0,0,1)
2. (1,0,1)
3. (1,0,0)
4. (0,1,0)
5. (0,1,1)
6. (1,1,1)
7. (1,1,0)
自分の右下方向の頂点7つを次のGeometryシェーダーに渡しています。
※(2019/12/8修正)隣接するグリッド数は7個では無く、13個でした。
自分でも勘違いしてたので、Unityで模型を作って確認してみました。
下図では、赤い球体は自身のグリッド、周りの青い球体は隣接するグリッド、水色の球体は今までの自信から参照する隣接するグリッドを表しています。
これに隣接するグリッドからの参照も表示したものが下図です。
線が多すぎてわかりにくいので、正面(Z軸方向を奥にして)から見てみます。
あれ?斜め上方向に線が繋がってないぞ…?ということがわかりました。
下図の赤い線です。
これはZ軸だけを見たものですが、他の軸から見ても同様なので、斜め方向にも参照を追加します。
下図の緑の球体と線が追加した参照です。
既存のグリッド数も合わせると全部で13個になります。
- (0,0,1)
- (1,0,1)
- (1,0,0)
- (0,1,0)
- (0,1,1)
- (1,1,1)
- (1,1,0)
- (1,-1,1)
- (1,-1,0)
- (1,-1,-1)
- (1,0,-1)
- (0,1,-1)
- (1,1,-1)
追加した参照の線も周辺グリッドに配置すると、隣接するグリッドすべてがちゃんと結ばれるようになりました(わかりにくいですが…)
おこうさん、ご指摘ありがとうございました!
で、話をもどしまして…
Geometryシェーダーは大きく2つの構成に分かれています。
前半は、自身のパーティクルの板ポリ(ビルボード)を作成するパート。
後半は、隣接する頂点を線で結ぶかの判定と実際の線の板ポリを作成するパートです。
struct v2g { float4 vertex : SV_POSITION; uint vid : TEXCOORD0; float scale : TEXCOORD1; float particleIntensity : TEXCOORD2; float lineIntensity : TEXCOORD3; float4 positions[7] : TEXCOORD4; }; struct g2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float intensity : TEXCOORD1; float3 color : TEXCOORD2; }; // 省略~~~~~ // ジオメトリシェーダ [maxvertexcount(82)] void geom(point v2g input[1], inout TriangleStream<g2f> outStream) { g2f output; float4 pos = input[0].vertex; float particleIntensity = input[0].particleIntensity; float lineIntensity = input[0].lineIntensity; float scale = input[0].scale; float3 color = hsv2rgb(float3(snoise(float4(pos.xyz * _ColorNoiseScale, _Time.y * _ColorNoiseSpeed)) * 0.5 + 0.5, _ColorSat, 1)); // ビルボード用の行列 float4x4 billboardMatrix = UNITY_MATRIX_V; billboardMatrix._m03 = billboardMatrix._m13 = billboardMatrix._m23 = billboardMatrix._m33 = 0; // パーティクル(点)の板ポリ作成 // 四角形になるように頂点を生産 for (int x = 0; x < 2; x++) { for (int y = 0; y < 2; y++) { // UV float2 uv = float2(x * 0.5, y); output.uv = uv; // 頂点位置を計算 output.pos = pos + mul(float4((float2(x, y) * 2 - float2(1, 1)) * scale, 0, 1), billboardMatrix); output.pos = mul(UNITY_MATRIX_VP, output.pos); output.intensity = particleIntensity; output.color = color; // ストリームに頂点を追加 outStream.Append(output); } } // トライアングルストリップを終了 outStream.RestartStrip(); // 線の処理 if (lineIntensity > 0.0) { // 近い点同士を線で結ぶ float3 cameraDiff = pos.xyz - _WorldSpaceCameraPos; float3 normal = normalize(cameraDiff); for (int i = 0; i < 13; i++) { float4 targetPos = input[0].positions[i]; // 点同士の距離を判定 float len = distance(targetPos, pos); if (len <= _ConectDist) { // 点と点を結ぶベクトル(線の方向) float3 dir = normalize(targetPos.xyz - pos.xyz); // 線の幅方向のベクトル float3 right = normalize(cross(dir, normal)) * _LineWidth * 0.5; // 線のポリゴンを構成する4頂点の座標 float4 v0 = mul(UNITY_MATRIX_VP, float4(pos.xyz - right, 1)); float4 v1 = mul(UNITY_MATRIX_VP, float4(pos.xyz + right, 1)); float4 v2 = mul(UNITY_MATRIX_VP, float4(targetPos.xyz - right, 1)); float4 v3 = mul(UNITY_MATRIX_VP, float4(targetPos.xyz + right, 1)); float3 targetColor = hsv2rgb(float3(snoise(float4(targetPos.xyz * _ColorNoiseScale, _Time.y * _ColorNoiseSpeed)) * 0.5 + 0.5, _ColorSat, 1)); // 点同士の距離に応じて線の明るさを変える(近いほど明るい) float distIntensity = 1 - smoothstep(0.0, 0.5, saturate(len / _ConectDist)); output.intensity = lineIntensity * distIntensity; // 線が見える時にだけ線を引く if (output.intensity > 0.0) { // triangle line output.pos = v0; output.uv = float2(0.5, 0); output.color = color; outStream.Append(output); output.pos = v2; output.uv = float2(0.5, 1); output.color = targetColor; outStream.Append(output); output.pos = v1; output.uv = float2(1, 0); output.color = color; outStream.Append(output); outStream.RestartStrip(); output.pos = v2; output.uv = float2(0.5, 1); output.color = targetColor; outStream.Append(output); output.pos = v3; output.uv = float2(1, 1); output.color = targetColor; outStream.Append(output); output.pos = v1; output.uv = float2(1, 0); output.color = color; outStream.Append(output); outStream.RestartStrip(); } } } } }
最後にFragmentシェーダーです。
特に複雑なことはしておらず、Geometryシェーダーで生成したポリゴンにUV座標に応じてテクスチャの色とパーティクルの明るさを乗算しているだけです。
struct g2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float intensity : TEXCOORD1; float3 color : TEXCOORD2; }; // 省略~~~~ fixed4 frag(g2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); col.rgb *= _Intensity * i.intensity * i.color; return col; }
スライダーによるパラメータの変更
リポジトリには入れてないんですが、実際のVRChatのワールドではパーティクルのパラメータをスライダーで調整できるようになっています。
スライダーのつまみの座標位置をシェーダーに渡すため、@yagiri_pgさんの「座標、姿勢をRenderTextureに書込、読込機構」を使わせていただきました。圧倒的感謝! yagiri.booth.pm
前述したとおり、VRChatはC#スクリプトが使えないので、オブジェクトの座標をシェーダーに正攻法では渡せません。
正直自分ではどうすればいいかわからなかったところに、@yagiri_pgさんが機構を公開してくれたので非常に助かりました。
実際に中身を見て驚愕したんですが、概要を書くと下記の通りです。
- Quadにシェーダーを割り当てる
- 特定のカメラにだけ映るようにレイヤー設定する
- 特定のカメラにRenderTextureを割り当てる
- 特定のカメラに該当Quadが映るときに、頂点シェーダーでカメラの指定した位置にQuadを移動させて座標を色にしてレンダリングさせる
こうすることで、RenderTextureの特定の位置に値を書き込めるという寸法らしいです(C#スクリプトなら1行くらいで済むことをやるのにこれだけの手順を踏まないといけないと知った時は辛い気持ちになった)
また、スライダーのパラメータの表示に@butadiene121さんの「数字表示シェーダー」を使わせていただきました。圧倒的感謝!
そしていままで私の配布してきたプレハブをまとめてました! BUTADIENE WORKS https://t.co/OHAYEqYUGY
— ブタジエン (@butadiene121) 2018年11月16日
GPUパーティクルとSAKURAとYUKIと、数字表示シェーダ、そしていままで作ってきたSDK関連のいくつかのギミックが入ってます。
全部無料ですのでよろしくお願いします~ pic.twitter.com/67UxnebrsG
まとめ
長々とした説明になってしまいましたが、まとめると以下のとおりです。
- 必要な頂点数分の点群メッシュを作成
- CustomRenderTextureでパーティクルの座標を更新
- VetexシェーダーでCustomRenderTextureから頂点座標を取り出して頂点の座標を移動
- Geometryシェーダーでパーティクルと線のポリゴンを作成
- VRChatのシェーダー芸人達は凄い
最後に、「Simple Plexus」ワールド作成に助力頂いた方々に圧倒的感謝!
※画像に載ってないけど初公開時にワールドに来てくださった方々に圧倒的感謝!
-
27個と書いてたけど、自身を抜いたら26個でした。↩
Thetaのストリーミング映像をリアルタイムにリフレクションさせてみた
本記事は Unity Advent Calendar 2018 の 18日目 の記事です。
はじめに
9/15のInfiniteRaveというイベントで、ThetaVを使って360度のストリーミング映像をSkyBoxにしたり、オブジェクトにリフレクションさせたりするVJシステムを作って使っていました。
当時はReflectionProbeを使って毎フレームSkyboxだけをターゲットにしてCubeMapを生成していたのですが、そのCubeMapのレンダリングがそこそこ重かったです。
60FPSは維持してましたが、他のオブジェクトなどの負荷も合わせると結構ギリギリなラインでした(そんなにつよつよPCではなかったせいもある)
上のアーカイブ動画の冒頭からスクリーンに映っている映像がソレです。
今回ReflectionProbeを使わずに、リアルタイムにリフレクションさせる仕組みを作ってみたのでそれの解説をします。
下が完成形の動画です。
「Unityを使ったVJでよく使うイメージエフェクト集」という内容でLTしてきた
11月9日、ドリコムで開催された「Unity Shader 勉強会」で「Unityを使ったVJでよく使うイメージエフェクト集」という内容でLTしてきました。
シェーダーに関する内容での発表を頼まれていたんですが、他の登壇者とかぶらないネタを考えてVJネタに落ち着きました。
通常のスライドと見せかけて、スライドにイメージエフェクトをかけて、見てる人をびっくりさせるためだけにUnityでスライドを作りました(背景色とかレイアウトもちょっとそれっぽくした)
uGUIを使ったスライド切り替えシステム自体はすぐ出来たんですが、作図が地味にめんどくさかったです。
VJネタはこういう技術系勉強会では珍しかったのか、Twitterでもなかなか好評だったと思います。
UnityエンジニアでもVJに興味を持つ人が増えるといいなと思います。
実際に見れるスライド
unityroomさんのほうで実際に動くスライドを公開しています。
音楽を聞きながら適当にイメージエフェクトをかけるとだんだん気持ちよくなるかもしれません。
Unityを使ったVJでよく使うイメージエフェクト集スライド | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう
スライドのソースコード
スライドのUnityプロジェクトもgithubの方で公開しています。
シェーダーのソースコードも入ってるので、詳しく見たい方はこちらを見てください。