[ョョョねこ Advent Calendar 2021] (https://adventar.org/calendars/7006) の25日目記事です。
メリークリスマス!
昨年はbrainfxxkでョョョねこからョョョねこを生成し再帰で増殖させるゴリ押しをさせて頂きました。
大体今年も似たような感じです。
##動機
Shader Fes 2021、開幕しましたね。
今回も出展物はもとより会場自体の作り込みもまた凄いことになっており、感嘆するばかりです。
そしてアイデアが無かった今年のョョョ成分としても良いヒントとなりました。
着想時点で12/20なのでだいぶ時間がありませんが、昔少しだけwebGL齧った知識を思い出しつつ、ョョョねこへの謁見が叶えばと思います。
やっていきましょう。
(注:初学者がもがいた記録なので記載内容は色々怪しいと思います)
#ねこは世界を歩く
我々の目は世界から飛び込んでくる光を捉えることにより、世界を見ることができています。
ということは、世界が存在しなくとも、同じように動く物を用意してやれば、そこに見える景色は世界と同じになります。
一般的にシェーダーといえば、モデル表面での見え方をなんかいい感じにしたりするやつですが、
実際のところは頂点の操作やピクセル毎の色決定ができ、これらは元となるモデルやテクスチャを必ずしも必要としません。
よって、全ピクセル上でねこを行進させ、それらのねこが到達する場所と色を計算してやることで、結果的に世界(の絵)を作り出すことができます。
かの有名な、ねこ・マーチングですね。
#ねこは誕生する
なにはともあれ作りましょう。
最終的にvrcに放り込める感じになればいいかなとか思ってたのでunity2019でやってます。
create -> shader -> unlit shader
とやると、以下のシェーダーが作られます。
だいたいC#っぽいノリで読めます。
Shader "Unlit/NewUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
前述のようにシェーダー上では頂点操作など色々と可能ですが、これらはパイプライン毎に分離されています。
全頂点に対して処理をする→全ピクセルの色決定を行う、といった流れになっており、
前者がvertex shader、後者がfragment shaderと呼ばれます。
もちろん頂点もピクセルもすごい数あるので、これらをGPUに放り投げて全部並行で計算させているとかなんとか。
(もっとガチな話になるとまた増えたりするらしいですが詳しくないので割愛します)
さて、上のシェーダーの中で、それぞれvertとfragがこれらに該当する箇所となります。
#pragmaのあたりでそれっぽい記述がありますね。
unityは偉いので、それぞれのシェーダーがどれなのかを決めておいてやれば、あとは勝手に運用してくれるようです。
このへんwebGLだとちゃんと受け渡しとかも書かなきゃいけなかった気がする(うろ覚え)ので便利でいいですね。
今回はピクセルごとの色計算を利用するため、fragment shader、すなわちfrag内に記載していきます。
##ねこは世界を視る
さて、何はともあれ作ったシェーダーをシーンに置いてみましょう。
適当にCubeとかPlaneとか設置してマテリアルを放り込みます。
当然ですがまっさらです。
さて、フラグメントシェーダーではピクセル毎に色を決定しているということでした。
つまり、fragが最後に返しているcolは色情報です。
float4とかfixed4とかの型が見えますが、これらは大体Vector4です。精度が違うだけですね。
各成分を抜き出したい時は.xyzwとか付けるとその順番通り都合良く抜き出した上でその型になってくれます。
というわけで適当に色を付けてみましょう。座標をそのまま色として放り込んでみます。
(最初から書かれている部分は一度消しています)
fixed4 frag (v2f i) : SV_Target
{
//rgba
fixed4 col = fixed4(i.uv.xy,0,1);
return col;
}
座標に従ってr,g成分に色が付きました。
全ピクセルに対して並列処理されているため、特に繰り返し等を書かずとも、1ピクセルへの処理を書くだけで全体に適用されます。
それでは、ねこに行進して頂きましょう。
##ねこは伸びをしている
まずは準備から。
とりあえず最初から書かれているうちのfogと名が付くものを消していきます。
その名の通りfogの処理で使われるようですが、今回は使いません。
またカリング設定も追加。
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Cull Front
次に、上では適当にその場にあったuv座標を放り込んでしまいましたが、ちゃんとしたメッシュの座標が欲しいため、これを渡す処理を追加します。
これはパイプライン前段であるバーテックスシェーダーから渡す情報の中に放り込みます。
ただしそのままだとローカル座標になってしまうため、ワールド座標に変換した上でフラグメントシェーダーへと渡します。
struct v2f
{
float2 uv : TEXCOORD0;
float3 pos : TEXCOORD1; //追加
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.pos = mul(unity_ObjectToWorld, v.vertex).xyz; //変更
o.uv = v.uv;
return o;
}
##ねこは丸まって眠る
お待たせしました。ねこを走らせていきましょう。適度な運動はねこの健康のためにも必要です。
基本的な流れとしては、ねこを大行進させ、衝突したねこには色を付けるという形です。
これらが全ピクセルで動作し、一つの画面が形成されます。
すなわち、
1.ねこと最も近い物体への距離を調べる
↓
2.ねこを進行方向にその距離分だけ進ませる
↓
3.何回か繰り返す
↓
4.最終的にねこと物体の距離が一定以下なら衝突したとする
となります。
ここで重要なのが1.の距離を調べる処理、すなわち距離関数で、ねこ・マーチングにおいて物体の形は大体コイツが司ることになります。
なにはともあれ見てましょう。これは中心が原点(0,0,0)にある半径1.0の球体の距離関数です。
float distanceFunc(float3 p){
return length(p) - 1.0f;
}
球の中心とねこの座標との距離を計算し、そこから半径分を引くことで、距離を導出しています。
ただしこれは中心が原点にあるため、球の中心とねこの座標の距離はlength(p)で計算できています。
これを使って、球体にねこをぶつけてみましょう。
なお、我が家のねこはレイちゃんという名前のため、コード中ではそのように書かれていることに注意してください。
fixed4 frag (v2f i) : SV_Target
{
float3 rayStartPos = _WorldSpaceCameraPos; //レイ初期地点をカメラ位置に
float3 rayDir = normalize(i.pos.xyz - rayStartPos); //レイ方向
float distance = 0.0; // レイ-物体間の最短距離
float rayLength = 0.0; // レイ総延長
float3 ray = float3(0,0,0); // レイ現在地
fixed4 col = float4(0,0,0,1);
for(int i = 0; i < 60; ++i){
distance = distanceFunc(ray); //距離計算
rayLength += distance; //レイ総延長に加算
ray = rayStartPos + rayDir * rayLength; //次のレイ位置算出
}
// hit check
if(abs(distance) < 0.001f){
//hit
col = float4(1,1,1,1);
}else{
//miss
discard;
}
return col;
}
一気に増えましたが、それほど複雑なことはしていません。
ねこの初期位置はカメラ位置、向きはカメラ位置とメッシュ座標から計算し、あとはその方向にひたすら走ってもらいます。
forループの中で、上に書いたようなねこ行進ループが行われています。
distanceFuncから最寄りの物体への距離が計算され、それをもとに行進させていくだけの処理です。
このときforループの回数は自由ですが、多くすればねこにより遠くまで行進してもらえる代わり、処理は重くなります。
本記事程度の例ならばともかく、もっと凝ったことをしていく場合はこのあたりの負荷問題がネックになってくるようなので、
各々ねこと対話していい感じにするとよいでしょう。
十分にねこが行進したら、それらのねこが衝突したかどうかをチェックします。
ある程度小さな値を閾値として用意し、物体への距離がそれよりも小さければ衝突しているものとして、白を描画しています。
衝突していない場合はdiscardして該当ピクセルへの描画を行わないようにします。
球体ができました。
もし何も出てこない場合、ワールド座標(0,0,0)地点を見ていない可能性があります。Plane等の座標を確認しましょう。
今回これは真っ白なので分かりづらいですが、しっかりと三次元の球体ができています。
距離関数を任意のものにすることにより、ねこの衝突具合を操作し、任意の形状を作ることができます。
それでは形を変えてみましょう。
##ねこは箱に入る
箱型の距離関数を作ってみましょう。
ねこに箱は付き物です。
float box(float3 p,float3 size){
return length(max(abs(p) - size, 0.0));
}
float distanceFunc(float3 p){
return box(p,float3(1,1,1));
}
イメージとしては、第一象限で四角形となるように式を立て、absを通すことで全体に拡張している感じです。
距離関数以外は特に手を入れなくて大丈夫です。
ねこを行進させてみましょう。
画像だとちょっと分かりづらいですが、カメラをぐりぐりするとしっかり箱型になっているのが分かるかと思います。
さて、ここからがねこ大行進のパワーです。
箱の距離関数におもむろに一項増やしてみます。
float box(float3 p,float3 size,float edge){
return length(max(abs(p) - size, 0.0)) - edge;
}
float distanceFunc(float3 p){
return box(p,float3(1,1,1),0.2);
}
バリアフリーな感じになりましたね。ねこにもやさしい。
物体の形を数式だけで表現することの利点が明確に出る場面かと思います。
さて、全部白いままだと分かりづらいので陰影処理をしてみましょう。
##ねこは暗所で落ち着く
ねこはハンターです。生来の習性から、暗所に潜み敵を狩るという行動をしたがります。
お猫様満足度を確保するため、光の概念を導入しましょう。
陰影処理をするため、法線処理を行い、ディレクショナルライトに照らされるようにしてみます。
まずはライトを取得するためのおまじないから。
SubShader
{
Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
LOD 100
次に、法線を計算する処理です。
小難しいことしてそうに見えますが、物体とねこの交点前後を取得することで勾配を出し、その勾配をそのまま法線としているだけですね。
float3 getNormal(float3 p){
float d = 0.0001;
return normalize( float3 (
distanceFunc(p + float3(d,0,0)) - distanceFunc(p + float3(-d,0,0)),
distanceFunc(p + float3(0,d,0)) - distanceFunc(p + float3(0,-d,0)),
distanceFunc(p + float3(0,0,d)) - distanceFunc(p + float3(0,0,-d))
));
}
ということでこれらを利用してライティングしてみます。
法線とライトを取得し、ランバート反射で照らしています。
このあたり真面目に理解を進めると終わらないので、とりあえずなんかいい感じに照らすやつ程度の認識で大丈夫です。
float3 normal = getNormal(p);
float3 lightDir = normalize(mul(unity_WorldToObject , _WorldSpaceLightPos0).xyz); //ライト方向のローカル座標化
float lambert = max(0, dot(normal, lightdir));
// hit check
if(abs(distance) < 0.001f){
//hit
col = float4(float3(1,1,1)*lambert,1);
}else{
//miss
discard;
}
それっぽくなりました。
暗所が大好きなねこたちもこれで安心です。
##ねこはョを手にする
距離関数を触ることで形状を変化させることができましたが、複雑な形状を一つの式にまとめるのには限界があります。
複数の物体を同居させてみましょう。
距離関数とは、最も近い物体への距離でした。
すなわち、すべての物体への距離を計算し、最も近いものを返せばよいということになります。
よって、各物体の距離関数を作り、それらのminを取ればよいということになります。
float distanceFunc(float3 p){
float yo1 = box(p,float3(1, 1, 1),0.1);
float yo2 = box(p-float3(-0.5, 0.4, 0),float3(1, 0.2, 1),0.1);
return min(yo1,yo2);
}
少しズレた場所にもう一つ横長の箱を作り、そこからminを取っています。
実行してみましょう。
箱が合成されました。
ところで、minを使って形状を作ってみたわけですから、maxの場合も見てみたくないですか?
ねこは好奇心旺盛ないきものですから、致し方のないことです。やってみましょう。
重なり合う部分だけが描画されました。つまり、先程のは論理計算でのor,こちらはandといった挙動をしています。
これもやはり数式として形状を表現しているわかりやすい利点になりますね。
また、図形合成はこれらに留まりません。引き算もできます。
2番目の箱をz方向に少し伸ばし、正負反転してmaxを取ってみます。
float distanceFunc(float3 p){
float yo1 = box(p,float3(1,1,1),0.1);
float yo2 = box(p-float3(-0.5,0.4,0),float3(1,0.2,1.1),0.1);
return max(yo1,-yo2);
}
図形がくり抜かれました。いかにもねこが好みそうな窪みですね。
このように数式を触っていくことで、キャットタワーのような難解な図形も自由に作ることができます。
行列を駆使することで図形をねじったりも可能です。各ご家庭のねこの好みに合わせて作ってあげてくださいね。
……ところで、この形は……
float distanceFunc(float3 p){
float yo1 = box(p,float3(1, 1, 1),0.1);
float yo2 = box(p-float3(-0.5,0.4,0),float3(1, 0.2, 1.1),0.1);
float yo3 = box(p-float3(-0.5,-0.4,0),float3(1, 0.2, 1.1),0.1);
return max( -yo3 , max(-yo2 , yo1) );
}
ョ……!!
##ねこは増殖する
なんということでしょう。
ねこはョの欠片を見つけてしまいました。
しかし、ョは一つでは意味を成しません。ョョョねこはョョョであるからョョョねこなのです。
あるべき姿へ戻してあげましょう。
float mod(float a, float b) {
return a - b * floor(a / b);
}
float3 trans(float3 p,float dist){
return float3(mod(p.x, dist) - dist/2 , mod(p.y, dist) - dist/2,mod(p.z, dist) - dist/2);
}
float distanceFunc(float3 p){
float yo1 = box(trans(p,9),float3(1, 1, 1),0.1);
float yo2 = box(trans(p,9)-float3(-0.5,0.4,0),float3(1, 0.2, 1.1),0.1);
float yo3 = box(trans(p,9)-float3(-0.5,-0.4,0),float3(1, 0.2, 1.1),0.1);
return max( -yo3 , max(-yo2 , yo1) );
}
何やらゴチャゴチャしましたが、座標の剰余を取って一定間隔でループさせるようにしただけのよくある処理ですね。
(HLSLにはfmodという関数が既に存在しており、GLSLで言うところのmodはこれに変換できるとの記載もところどころ見られますが、実際は負数での挙動が異なっており、自分でmodを書いた方が良いようです。)
そして、これによりねこは真の姿を取り戻します。
ョョョねこは増殖します。forループの回転数を上げると更に遠くまで増殖します。
ョであるから、ョョョとなります。
##ョョョねこは1677万色に輝く
ねこは輝きます。ねこはいます。
皆様のよき年末に、傍らのョョョねこを。
ありがとうございました。
◆「よい おとしを!