Shader関連

note: 私 (phi16) が知識無い時に書いたものが多分に含まれているので、適切な記事を探したほうが良いと思います…。今ならあると思うので (2021/06/13)

※未整理情報です、後でページの形式が変更されるかもしれません
※間違っている部分があれば修正お願いします

Shaderとは

物体(Mesh)の見た目を制御するプログラムです。Unityには大きく分けて2種類のシェーダがあります。

  • Surface Shader : Unityが持つライトの情報などが自動的に適用される、現実的物体向けシェーダ
  • Fragment Shader : 画面に表示する色をダイレクトに決定する、色を確定したい場合向けシェーダ

またそれぞれの表示色制御の前に、物体の位置を書き換える Vertex Shader があります。
(ポリゴンをプログラムで生成するGeometry Shaderもちゃんと使えます。)

一般的な利用方法は以下です。

  • まず Vertex Shader によって物体の位置や回転を適用させる (そうしなければワールド原点に表示されてしまいます)
  • Fragment Shader によって空間上のあるピクセルの色を計算する (面の向きを使ってライトを表現したり)
  • Surface Shader ではピクセルの色について、ライトや反射などの計算をUnityに全て任せることができる
    • ちなみに Surface Shader は最終的に Fragment Shader に自動的に変換されます

シェーダプログラムは ShaderLab という言語で書くことになります。
大まかには「Unityの描画エンジンに関する情報」と「具体的なシェーダプログラム」で構成されます。前者は裏面表示や描画順制御などを制御するものです。

基本的には最初に自動生成されるコードを弄れば、メインプログラムだけを書いたら動くような状態になると思います。

Shader から使える値

シェーダ内部では何らかの値に基づいて色を決定しますが、そのときに使える情報にはかなり限りがあります。

Unityが提供するもの

こちらにあるものです。代表的なのを以下に記します。

  • unity_ObjectToWorld : (ほぼ)Transformの情報 (4x4正方行列)
  • _WorldSpaceCameraPos : 描画を行うカメラの位置 (3次元ベクトル)
  • _Time : シーンを開始してからの経過時間 (VRChatだとワールドに入ってからの時間だと思います、各ユーザで独立)
    • (4次元ベクトルですが、y コンポーネントが秒単位です)
  • unity_DeltaTime : 前回のフレームからの経過時間

Shaderで設定するもの

Unity側から自由に値を設定することができるPropertyをつける事ができます。

  • テクスチャ
  • Float値 / Int値
  • 4次元ベクトル (色や位置などに使える)

この辺の値はAnimationからも制御できます。

Shader Program

公式リファレンスがもちろん詳しいと思いますが 簡単に書いておきます

Unlit系 (Fragment Shader)


基本は、「頂点データを受け取る」「Vertex Shaderで描画位置を決定・追加情報を生成」「Fragment Shaderで色を決定」という流れです。

関数名は好きに決められますが以降 vert (Vertex Shader) と frag (Fragment Shader) で記述します。

  • 頂点データとして受け取るものと、Vertex Shaderから出力してFragment Shaderに渡るものの構造を前もって定義します。(struct)
    • それが何であるか、は semantics という形で宣言します。ref
    • Vertex Shaderから好きな値を吐きたいときは TEXCOORD(数字)を使えばいいと思います
  • vert 関数には先程定義した頂点データが入ってきます。
    • 位置情報にTransformのScale値が微妙に適用されてる気がします。要検証
    • モデルのUV座標や法線方向が取れます。
  • Vertex Shaderの返す値における、POSITION semantics の値が「画面上の描画位置」になります。
    • 4次元ベクトル (x,y,z,w) ですが、そのうちの (x/w,y/w) の位置に描画されます
      • このスクリーン座標は左下を (0,0)、右上を (1,1) としています。
      • (zはdepth情報として使われ、遠すぎるオブジェクトを消すために使われたりします)
      • 詳細は透視変換などで調べるといいです
    • 基本的には UnityObjectToClipPosunity_ObjectToWorld を使えば気にしなくて良いです
  • frag 関数の引数は vert の返した値ですが、ピクセル位置によって線形補間 されます。
    • UVが(0,0)の頂点と(0,1)の頂点(とあともう一点)からなるポリゴンを描画した際、その頂点対の中間位置のピクセルの色を決定する際にはUVの値を(0,0.5)として受け取ることになります。
    • 1920x1080の領域をレンダリングすることになれば、この関数が200万回実行されます。GPUはすごい。
    • Normalも線形補間されますが、長さが1じゃなくなると思うので適宜normalizeしてあげる必要があります
  • あとは好きな色を4次元ベクトル(RGBA)として出力するだけです。
    • 例えば、デフォルトの設定の元で return float4(i.uv,0,1); と書けば赤と緑のグラデーションができるはずです

Surface Shader


Surface ShaderではUnityが元々備えている描画機構によって色を決定してもらう、という事ができます。
Vertex Shaderの動作はほぼ Fragment Shader のときと同じですが、差はいくらかあります。(examplesを見るのが一番よさそう)

Surface Shader の出力は、「ベース色(Albedo)」「法線方向(Normal)」「発光色(Emission)」「環境の反射(Metallic)」「表面の滑らかさ(Smoothness)」などで、あとは自動的に具体的な色を決定してくれます。
Surface Shader無しでライトなどを正しく適用するのはかなり大変です (ref1, ref2)

Camera / Render Textureとの併用

Unityにはカメラがありますが、カメラの吐いたRender Textureをシェーダで参照し、それを表示した板をカメラで撮影するというループを作ることで状態管理ができます。

状態管理ができるということはゲームなどが作れます。描画内容は元々好きに出来ますし、状態もテクスチャ分だけあるので何も困ることはないです。

lg_feedback.png

具体的な作り方は以下。

  • Render Texture を作ります。
  • Shader(Unlit) を作り、それを読む Material を作ります。
  • Material の property の MainTex として、先程の Render Texture を設定します。
  • Plane を適当に置いて、この Material を適用します。
    • これ書いたときは Plane って書いたんですが、もちろん Quad のほうがいいですね・・・
  • その Plane を直に見る Camera を作ります。
    • Projection を Orthogonalにする
    • Size を Plane の大きさに完璧に合わせる (高さが揃っていればOK)
    • Near Clip を 0、Far Clip を 0.15 くらいで Plane の目の前に於けば大丈夫です
      • Size/Clip領域が小さい方が描画が軽いはずです (不要なオブジェクトを描画しないという機構がよく働く)
      • 追記: Cameraの位置を並行移動して Near=0.5, Far=0.65 のようにしたほうが描画が安定します
    • この機構が誤動作するのを防ぎたい場合は Culling Mask で Plane のみを描画させるようにすればいいです
      • 何もしないとカメラを遮るように人やオブジェクトがあればもちろん誤動作します
      • 新規 Layer として Plane のみが所属するものを作れば良さそうです
    • Target Texture を先程の Render Texture にする
    • Allow MSAAを切る
      • これをしないと普通の物体を写したときにボケる (本来はいいこと)
  • Render Texture の大きさを 1x1 くらいにする
    • これはまさしく保持できる状態量なので、目的に合わせて大きくしたりしてください
    • 描画に掛かるコストはこの面積に比例するはずなので、小さければ小さいほど良いです
  • (ちなみにデフォルトの Color Format だと1ピクセルに32bit(8,8,8,8)の情報が載ります)
    • が、Alphaはおそらく?使えないので実質 24bit
    • 浮動小数点を直に保持したい場合は ARGB Float を使えばいいと思います
      • 使ったらクラッシュ率が爆上がりしたのでおすすめしません
      • Unity2017で修正されたらしい(未確認)
      • 浮動小数1つ32bitを頑張って8bitに分解して保存するのが現在の最善な気がします
  • Render Texture の Filter Mode を Point にする
    • Bilinearだとボケます (読み込むテクスチャ座標を完璧に合わせればいけますが)

これでループが完成するので、あとは好きにシェーダを書き換えればいいです。

  • 前回の状態はまさしく fixed4 col = tex2D(_MainTex, i.uv); です。
    • カメラが回転してるとUVも回転するので適宜直してください (表面・裏面でも変わります)

Shaderを使ってできることとできないこと

できること

  • ポリゴン上でのアニメーション
    • 参照するUV座標は好きに操作できて、時間を読むこともできる
  • 好きな絵を描く
    • プログラムによって円や矩形などを描画することが出来ます
      • if(length(i.uv - float2(0.5,0.5)) < 0.5) col.r = 1; とすれば中心に赤い円ができる
      • 極端なことを言えば3Dのレンダリングもできます (レイトレーシングすれば良い)
      • Shadertoy っていうサイトにその具体例がいっぱいあります (ブラウザクラッシュするかも)
      • このサイトもテクスチャのループを作れるのでゲームが作れます (ちなみにこれはGLSLっていうちょっと違う言語です)
  • 物体のTransformの取得
    • unity_ObjectToWorldの左上の3x3行列成分が回転行列、あと(0,0,0,1)を突っ込んで掛け算すればその原点位置が取れます
    • その情報をテクスチャに出力すれば他のシェーダから「そのオブジェクトの位置」が取れます
    • 回転行列から傾きがわかります
    • スケール成分については簡単に取るのは怪しいかもしれません (微妙に変換済みっぽい挙動をする)
  • Triggerを検知
    • Animatorとして「一瞬だけ色が変化する」ものを作ってそれをカメラで撮ればボタンの押下を検出できます
    • MaterialのパラメータをAnimationで直接いじってもいいと思います
  • 物の検知
    • 決まった色をした物体を置いておいて、カメラが特定の位置にきたときにテクスチャにその物体が映り込むこととを検知することが出来ます
    • 映っている物体の位置を知ることは出来ません 詳細はできないことについての方で。
  • 物体の変形
    • Vertex Shader を使えば拡大縮小や簡単な移動はやり放題です。
    • UV座標を元に変換先を決めてもいいですし、元々の頂点位置も使えます。
  • ゲーム (表示だけなら)
  • 物の一部を消す
    • 多分描画順序でも制御できますが、clip(-1); ってシェーダで書くと消えます。これは条件文の中でも作用します。
    • 本来は「0未満の値が来たときに消える」ので、そのような入力があるならばその方がシンプルかもしれません
    • 基本的にシェーダでは条件分岐は避けるべき、という話もあります

できないこと

  • Unity上の物体の移動
    • 基本的にShaderでは「見た目」しか変更できません。Colliderも変化しません。
    • カメラ位置を変化させる必要があるので上の理由により厳しい。
    • Portal作りたかった・・・
  • シェーダの出力に依って音を鳴らす
    • 効果音のついたゲームは作れなそうです (ボタン押したときに音がなるのはできそう)
  • カメラに映る物体の位置検出
    • 時間(フレーム数)を掛ければ出来ますが、簡単に拾う方法はありません。
      • 時間を掛けるっていうのはテクスチャを数フレーム掛けて何度も走査するという話です
      • 1フレームにテクスチャを大量に見るのは負荷的によろしくないと思います
      • 高々10サンプリング程度が良さそうだと個人的に思ってます (負荷検証次第ではわかりません)
  • ボタンを押すたびに状態管理を持った物体をSpawnさせる
    • Render Textureが増えないので出来ない気がします (未検証・・・)
  • プレイヤーの視点位置の保存
    • カメラで撮ろうとするとそこが視点位置になってしまう

Render Textureの色変化からTriggerを走らせられると世界がやばいことになります (大技術革新) (実質スクリプティング) (できなそう)

出来ないと思ってるけどできるようになるかもしれない(限定的にでも?)ので、何かあれば更新してください

注意点・テクニック

  • Vertex Shader から Texture を参照する際には tex2D ではなく tex2Dlod
  • (w,h)サイズのテクスチャの、左下から(x,y)ピクセル目の色を参照するUV座標は ((x+0.5)/w, (y+0.5)/h) (丁寧な検証はしてないです)
  • Surface Shader における Vertex Shader の出力頂点はモデル座標系なので、ワールドにおける位置 p に出したい場合は v.vertex = mul(unity_WorldToObject, p); する必要
  • Unlit (Fragment Shader) ではデフォルトだと影がつかないので以下の Rendering Path を追加する (これはどっかで見つけてきたやつなので、もっと簡便な方法があるかもしれません)

Pass {
    Tags {"LightMode"="ShadowCaster"}

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_shadowcaster
    #include "UnityCG.cginc"

    struct v2f { 
        V2F_SHADOW_CASTER;
    };

    v2f vert(appdata_base v)
    {
        v2f o;
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
        return o;
    }

    float4 frag(v2f i) : SV_Target
    {
        SHADOW_CASTER_FRAGMENT(i)
    }
    ENDCG
}

  • Unity の Dynamic Batching が有効だと複数オブジェクトを一気に描画しようとして、Vertex Shaderに入ってくるモデルの頂点がTransform適用後のものになってしまう
    • Tag として "DisableBatching"="True" を追加すれば無効になります
  • 色を検知しようとしたとき、Unity上で設定した色だとgammaの影響で比較がうまくいかないことがありました
    • Unlit/colorやUnlit/textureで表示したものをカメラで撮ったりする場合。(シェーダの出力は何もしなくてOK)
    • pow(x,1/2.2) して計算するといいです
  • Vertex Shaderで頂点を外側に動かした場合、Bounds の外に出てしまうせいで視界の隅だと消えてしまうことがある
    • Skinned Mesh Renderer を使って Bounds を直接広げることで回避できる → 2019アップデートで制限がついたみたいです
  • 物体との距離はRenderTextureのColor FormatをDepthにすればdepthが拾えるのでいい感じに計算してあげると取れます
    • 取るときには sampler2D_float でテクスチャを宣言するようです
    • ちなみに2x2で拾って勾配を取れば衝突面の法線がわかります
  • 「物を映さない方法」
    • 最も簡単なのはLayer (Avatarでも一度WorldとしてinitializeすればLayerMatrixが得られる)
      • ただちょっと怖いかも?(わからない)
    • カメラ検出で特定の物体を映さないことができる
      • UNITY_MATRIX_P[3][3]==1 のとき Orthogonal Camera (普通の視界は Perspective (0が入っている))
      • カメラの出力先テクスチャ解像度 _ScreenParams.xy
      • (ほぼ上と同じだが) カメラ自体のアス比 - UNITY_MATRIX_P[1][1] / UNITY_MATRIX_P[0][0]
      • 鏡の中だと P[2][2]0 ではなくなる
      • 「特定の値であるか」を調べるには一般に abs(x-a) < 0.001 みたいな式を使うと良い (浮動小数点誤差がある為)
    • あとStencilを使って「特定の板(隠れてる)」を通さないと見えない物体をつくることもできる
    • 普段の視界に映らなければ平気でRenderQueueを持ち上げても大丈夫
      • 精度確保の為に視界ジャックをやっても安全
      • Shader芸が干渉する可能性は無いとは言えないけど
  • 一部のワールドでカメラがうまく動かない場合
    • カメラと描画対象のサイズを大きくすると解決することがあります (butadieneさんより)
      • その際に上のカメラ検出を使って見えないようにすると良さそう

未解決問題

  • Avatarに仕込んだカメラが、一部のワールドでは正常に物を映さない
    • Far Clip を必要以上に大きくしないと表示されない、また位置や方向に依存する
    • Japan Town で顕著、また Gaia Night など
  • Avatarに仕込んだカメラに、稀に真っ黒もしくは白いポリゴンが映る、および半透明物質のdepthが正常に計算されていないような表示が起きる
    • The HUB もしくは The Old HUB で確認
  • Avatarに仕込んだフィードバック機構が稀にリセットする
    • 状態が吹っ飛ぶ
    • 任意のワールドで起きる

ちょっと便利なもの

色をint値にEncode/Decode

uint v = 0;
v *= 256; v += int(c.r * 255.);
v *= 256; v += int(c.g * 255.);
v *= 256; v += int(c.b * 255.);

float4 c = float4(0,0,0,1);
c.b = (v-v/256*256+0.5)/255, v/=256;
c.g = (v-v/256*256+0.5)/255, v/=256;
c.r = (v-v/256*256+0.5)/255, v/=256;

乱数

float rand(float2 co){
    return frac(sin(dot(co.xy, float2(12.9898,78.233))) * 43758.5453);
}

いろんなところで使われている

float3を4ピクセルのfixed3に圧縮・展開する機構

浮動小数を保存したいときはこれ使えばいいと思います (浮動小数点テクスチャは不安定のようです)


そのままでは使えません (わかる人向けということで)

float3 pack(float3 xyz, uint ix) {
    uint3 xyzI = asuint(xyz);
    xyzI = (xyzI >> (ix * 8)) % 256;
    return (float3(xyzI) + 0.5) / 255.0;
}

// その値を保持する領域の上のUV座標を uv として
// pack(col, int(uv.x*4.0)) を保存する

float3 unpack(float2 uv) {
    float texWidth = 8.0; // RenderTextureの横幅
    float3 e = float3(1.0/texWidth/2, 3.0/texWidth/2, 0);
    uint3 v0 = uint3(tex2Dlod(_Pos, float4(uv - e.yz,0,0)).xyz * 255. + 0.5) << 0;
    uint3 v1 = uint3(tex2Dlod(_Pos, float4(uv - e.xz,0,0)).xyz * 255. + 0.5) << 8;
    uint3 v2 = uint3(tex2Dlod(_Pos, float4(uv + e.xz,0,0)).xyz * 255. + 0.5) << 16;
    uint3 v3 = uint3(tex2Dlod(_Pos, float4(uv + e.yz,0,0)).xyz * 255. + 0.5) << 24;
    uint3 v = v0 + v1 + v2 + v3;
    return asfloat(v);
}

// 領域中心を uv として、左に2px、右に2px使っている場合のコード
// よしなに修正してください

使いやすく&わかりやすくしてくれる方はぜひお願いします

Shaderによる2Dゲーム開発ライブラリ (yamikumo)

上記の原理をラップ・汎用化したライブラリのベータ版を以下で配布しています
具体的には、各ピクセルを一次元ベクトルで管理しインデックスを自動で割り当てる事で、どのピクセルにどの情報を保存したのか、プログラマが全く意識することなくデータを管理できるようにしています
既知のバグ・修正


資料など

サポーターズコラボさん主催のUnityシェーダー勉強会のまとめ

フラグメントシェーダーについてのガイドブック

視野と解像度の取得方法と対応付け

wについて

画面端でも破たんしないmatcapシェーダーの作り方(ShaderForge)

コメント・情報提供

コメントを投稿するには画像の文字を半角数字で入力してください。


画像認証

カテゴリ・タグ: カテゴリ-Shader

  • 最終更新:2021-08-12 15:59:25

このWIKIを編集するにはパスワード入力が必要です

認証パスワード