Shader芸例

※他の人が作ったものなども置いてほしいので適宜いじってください

以下のものが実際に置いてあるワールドがあるので 自由に遊びにきてください
  • Laine's Lab : wrld_f749cd1f-3b02-439b-ad6e-153a1eb2fdd3 (作者:phi16)

傾く無限ワイングラス

  • 原理: 回転行列から水面を計算
  • 描画時に「水面になるであろう高さ」を計算し、その位置よりも高いピクセルを消す

モデルの大きさが良くわからなかったので実際に実験して高さを調べたため、ちょっと変なコードになってます

// vert
o.rotPos = mul((float3x3)unity_ObjectToWorld, v.vertex.xyz); // 平行移動成分を無視して回転

// frag
fixed4 col = _MainColor;
float top = mul((float3x3)unity_ObjectToWorld, float3(0,0,0.17305)).y; // これで -1 ~ 1 の値
                                                                       // モデルによって値は変わります
float theta = acos(top); // 角度を調べる
float level = sin(theta) * (-0.046) + cos(theta) * 0.03; // 最高点だと思われる場所を計算
if(i.rotPos.y > level | | theta > 1.55) clip(-1.0);
// 表示位置が最高点以下の場合clip (あと下を向きすぎていてもclip)

return col;

これだけです。これは Fragment Shader でピクセルを消しているだけなので指を突っ込んでも指が見えます (蓋がありません)


ライフゲーム

  • 原理: テクスチャ全点が並列に動くので各点で書くだけで動く (普通の状態保持)
  • 1F毎にライフゲームを処理しつつ、外にあるカメラに物が映った場合に適度にセルを追加

入力テクスチャとして「前状態」と「入力用カメラ」を拾い、各ピクセルで前状態を 9 回サンプリング。

float2 pos = float2(i.uv.y, 1.0-i.uv.x); // カメラが回転してるので適当に調整 (カメラ側をいじってもいい)
float col;
float3 e = float3(1.0 / 128.0, -1.0 / 128.0, 0.0); // 「1ピクセル」の差が 1/128 です
if(_Time.y < 3.0) {
    col = 0.0; // ロード後3秒間は常に全セルが Off (初期化用)
} else {
    int count = 0;
    count += tex2D(_MainTex, pos + e.xx).r > 0.5 ? 1 : 0; // 周囲8マスを拾う
    count += tex2D(_MainTex, pos + e.xz).r > 0.5 ? 1 : 0;
    count += tex2D(_MainTex, pos + e.xy).r > 0.5 ? 1 : 0;
    count += tex2D(_MainTex, pos + e.zy).r > 0.5 ? 1 : 0;
    count += tex2D(_MainTex, pos + e.yy).r > 0.5 ? 1 : 0;
    count += tex2D(_MainTex, pos + e.yz).r > 0.5 ? 1 : 0;
    count += tex2D(_MainTex, pos + e.yx).r > 0.5 ? 1 : 0;
    count += tex2D(_MainTex, pos + e.zx).r > 0.5 ? 1 : 0;
    int cur = tex2D(_MainTex, pos) > 0.5 ? 1 : 0; // あとはルールに従って色を計算
    col = (cur == 1 && (count == 2 | | count == 3) | | cur == 0 && count == 3) ? 1.0 : 0.0;
}
float3 inp = tex2D(_InputTex, pos.yx).xyz;
if(length(inp) > 0.001) { // 入力テクスチャが黒でない場合に乱数で色を決定
   col = rand(_Time.xy + pos);
}
return float4(float3(col, col, col), 1.0);

壁の裏は黒い板が置いてあります
さわっているボードの方は別のシェーダで (Surface Shader) で、単に色を見てclipしつつ金属っぽい出力をしています

ゲーム機 (Flappy Bird)

状態保持ができるので後は好きにすれば良いという感じです

  • ボタンの入力はTriggerにしてAnimatorで一瞬色を変更させるのを検知
  • 状態は「位置(0~2)」「速度(-0.1~0.1)」「スコア(0~16777215)」「ハイスコア(0~16777215)」
    • 位置・速度はそのまま範囲を 0~1 に変換してR成分で保持、スコア・ハイスコアはint値を色にエンコードして保持
  • テクスチャは 2x2 で、それぞれが1pxずつ使ってます
    • テクスチャの参照位置は (0.25,0.25) とかです
  • 書き込みをする際には「自分が誰か」をチェックして書き込み
    • つまり、位置を出力する人は速度を計算していますが、書き込むものは位置のみ。

入力部分

float4 posC = tex2D(_MainTex, float2(0.25,0.25));
float4 velC = tex2D(_MainTex, float2(0.25,0.75));
float4 scoreC = tex2D(_MainTex, float2(0.75,0.25));
float4 highscoreC = tex2D(_MainTex, float2(0.75,0.75));
float pos = posC.r * 2.0;
float vel = (velC.r - 0.5) * 0.2;
uint score = 0;
score *= 256; score += int(scoreC.r * 255.); // 関数化すれば良さそう
score *= 256; score += int(scoreC.g * 255.);
score *= 256; score += int(scoreC.b * 255.);
uint highscore = 0;
highscore *= 256; highscore += int(highscoreC.r * 255.);
highscore *= 256; highscore += int(highscoreC.g * 255.);
highscore *= 256; highscore += int(highscoreC.b * 255.);

出力部分

if(i.uv.x < 0.5){
    if(i.uv.y < 0.5) { // pos
        col.r = pos / 2.0;
    } else { // vel
        col.r = vel / 0.2 + 0.5;
    }
} else {
    if(i.uv.y < 0.5) { // score
        uint c = score;
        col.b = float(c-c/256*256+0.5)/255., c/=256;
        col.g = float(c-c/256*256+0.5)/255., c/=256;
        col.r = float(c-c/256*256+0.5)/255., c/=256;
    } else { // highscore
        uint c = highscore;
        col.b = float(c-c/256*256+0.5)/255., c/=256;
        col.g = float(c-c/256*256+0.5)/255., c/=256;
        col.r = float(c-c/256*256+0.5)/255., c/=256;
    }
}

中身はこんなことになってます (入力用と状態保持用)

flappy_sh.png

スコアの表示は、メモリにあるint数値を桁で分解して描画位置によって異なるテクスチャ位置を参照させてやってます

// Display Score (d が自身の表示座標)
if(d.x >= 0.3 && d.x <= 0.9 && d.y >= 0.8 && d.y <= 0.9) { // 0.6 x 0.1 の領域
    float2 axis = (d-float2(0.3,0.8)) / 0.1; // (0,0) ~ (6,1) の座標系
    uint target = floor(axis.x); // 自身の表示桁
    axis.x -= float(target); // axis.x はこれで 0 ~ 1
    for(uint i=0;i<6;i++) {
        uint digit = score - score / 10 * 10; // 桁を計算
        float2 uvPos = float2(digit,0) + axis; // それに従ってテクスチャの位置を計算
        if(i + target == 5) { // 今参照しているのが自身の表示桁であるときに
            float e = tex2D(_Number, uvPos / float2(16.0,1.0)).r;
            if(e < 0.5) col.rgb = float3(1,1,1); // テクスチャを参照
        }
        score /= 10;
    }
}

飲めるビールジョッキ

  • 原理: ワイングラスに状態保持を付け加えたもの
  • 「最も低い座標からの高さ」を保つようにしています (丁寧に計算するのは面倒なので)
  • ワインと同じ方法 (Fragment Shader) だと上から除いたときに液体部分 (黄色) が見えてしまうので、Vertex Shader で処理しています
    • 液体表面の位置を逆算して、「頂点がどこまで行けば液面なのか」を計算してます
    • 頂点のスケールなどがごちゃごちゃしてたのでまた筋肉で書いてます

// (rotated vertex).y == level
float rotated = mul((float3x3)unity_ObjectToWorld, v.vertex * 100.0).y;
float zContrib = mul((float3x3)unity_ObjectToWorld, float3(0,0,1)).y;
                    
if(zContrib <= 0.0) {
    v.vertex.z = 0.0;
} else {
    // rotated + zContrib * dz == level
    v.vertex.z += (level - rotated) / zContrib / 100.0;
}

  • Fragment Shader の方で「ジョッキの高さより下」にあるピクセルをclipすることでいい感じにしています

ついでにカメラがちょっと回りを見ていて、真っ黄色のオブジェクトがあったときにそれを認識して水位に元に戻したりします

ろくろ回し

原理
  • 32x32テクスチャで頂点をいじれば自由に変形できる
    • あのポットのモデルは円周方向32分割、縦方向32分割されてます
    • ポットの中にこのRenderTextureの状態保持機構が入っています
  • 赤球の中心にPlaneがあって「赤球の位置」を出力し、それをカメラでテクスチャに保存
  • ポットの位置更新をするシェーダでは、各頂点に対して赤球が近くにあるかどうかを調べて反応
    • 球の高さ方向の軸からの距離を見て、内側に移動するか外側に移動するかを決定
  • ポットの設置について
    • ポットの原点位置がろくろの原点位置(固定)に近いかどうかを判定し、位置更新を実際に行うかを判断
    • ついでに原点位置に近いときには頂点シェーダでTransformがある固定の値であるように振る舞わせる
      • rotationもガン無視なので設置した瞬間に向きが変わります (直せば直る)
  • 表示するシェーダは Surface Shader ですが、このままでは Normal が正しくない
    • ので、現在位置の周辺4pxの出っ張り高さを拾ってきて法線方向を適当にごまかす

o.Normal = IN.inner > 0.5
  ? normalize(float3(IN.right - IN.left,IN.down - IN.up,1))
  : normalize(float3(IN.right - IN.left,IN.up - IN.down,1));

  • 回転について
    • 回転速度などは別のテクスチャの色をAnimatorで制御
      • ついでにそれを見る状態保持機構が回転角度を持っています
    • 実際に回転させるのは頂点シェーダ
    • 位置更新側でもこの回転を拾う必要がある (当たる場所が変わるため)
    • ろくろの使いやすさを上げるために回転が速いときには赤球が当たる範囲を適当に広げています

// わかる人向け
float origDir = atan2(origV.z, origV.x);
float stickDir = atan2(i.stick.z, i.stick.x);
if(abs(fmod(origDir - stickDir + 3.1415926535, 2 * 3.1415926535) - 3.1415926535) < rotation.r) {
    origDir = stickDir;
    float len = length(origV.xz);
    origV.xz = float2(cos(origDir), sin(origDir)) * len;
}

  • Reset用にもまたそういうテクスチャを作って参照させている

フローチャートボードと迷路探索

出来る限り「同期ズレ」が起きないように設計したもの つまり物理環境をできるだけ利用するようにしている

原理
  • キューブの6面に2x2のテクスチャを張る(全部で24通り)。カメラからこれを写せば各マスに置いてあるキューブの向きと角度が計算可能。
    • そしてこれは完全に物理情報のみによって決まるので、同期が可能
  • このデータを元に、道を(自己フィードバックしながら)生成。各マスは周囲4マスの出力を見て道を構築していく。道が完全に完成するまで8フレーム掛かる。
    • このときエラーが発生するような配置なら、それも出力。
    • そして「64マスのどこかにエラーが存在する」かどうかを検出するために、8x8→4x4→2x2→1x1と畳み込んでいく。
      • 各マスは一回り大きいボードの4ピクセルをサンプリングする。計算結果が出るまでに4フレーム掛かる。
  • フィールドの各マスには2種類の物体を同時に設置(片方のみ有効)、特定のColliderが当たったときにonEnterTriggerで切り替える (これも同期できる)
    • 判定用に2つのレイヤーを使っている
    • そして片方の物体にはカメラ用に白い物体を仕込んでおく。そうすれば盤面がテクスチャとして得られる。
  • キャラクタ・ゴールの位置を得るためにそれぞれモデルの中に位置調査用のQuadとカメラが仕込まれている
  • あとは好きに計算する
    • これ以上の同期は不可能(プログラムがいつ停止するかわからない為)。そこでロック・アンロック時に状態リセットを掛けることで「1度ロックを動かしたら必ず同期する」ようにした。(実用上十分)
    • ちなみにキャラクタ位置は頂点シェーダで動かしています

おまけ (一般的な話なのでどっかに持っていきたい) (似たような話がどこかにあったような気がする)

ロック・アンロックはAnimationTriggerで動かしています(lock/unlock trigger)が、単純に2状態を繋いだだけだと問題が起きる。各々の状態に自己ループを追加してtriggerを消費することで回避

  • 各々の環境でtriggerが「いくつくるか」がわからない。そこでtriggerがどう送られていても安定して動作させる必要がある。
  • triggerは消費されない限り残り続ける。つまり(自分の環境で)lockedのときにlock triggerが送られてしまうと、それは溜まって unlockした瞬間にlockが走って元に戻る。
  • 一度これが発生すると、異なる状態に居る人とは永遠に同期されない(どちらかの人が必ず1つ余分にtriggerを持っていることになる)。
  • これを回避する方法は、triggerを消費させるか(状態に自己ループを追加)、イベントを1元化するか(AnimationBoolで2つを区別する)。

メジャー


原理
  • TrailRendererが生成するポリゴンにはTexture Modeに応じて適切なUVが張られる。
  • そのうち Tile では、U座標が物体からの距離に対応している(適切なスケーリングは必要)。(V座標はいつもどおりの[0,1])
  • そこでこのU座標を使って絵を描けば、距離に応じて線を引いたりなど出来る
    • 実際のプログラムは上のリンクに載っているので参照

元々はTrailRendererを使って状態保持が出来ないか考えていて思いついたもの。残念ながら「物体からの距離」なのでそれは不可能であった。(位置が安定しない)

GPUパーティクル


原理
  • 32768ポリゴンをgeometry shaderで移動させて各々を1パーティクルにする
    • このときにローカルUV座標を計算させて、円形にクリッピング
  • 各パーティクルはテクスチャ8pixel分で表現
    • 位置と頂点(ともにfloat3)を直接エンコードしてpacking
    • 更新式は雑に (指定した点に向かう加速度を好きに作る)

触れる水

実物はパブリックワールドの"WavePool"(作者 Raku_Phys)においてあります。

触れる水は、水面の状態をシミュレーションする平面と、実際の水面の二つの部分がある。下記ではこれら二つと、水面に触れる仕組みの3項目に分けて原理を説明する。

1.シミュレーションパート

  • 水面の状態のシミュレーションは波動方程式を差分化したもので計算される。波動方程式は二階の微分方程式なので、次の状態を決定するには2F前までの状態の情報が必要になる。そこで、カメラとレンダーテクスチャを利用した次のような方法を用いた。

    • 適当な大きさのQuadとカメラ+レンダーテクスチャを用意して状態管理の機構を作る(シェーダー関連の項を参照。)
    • Quad上でRed(0~255)を使って水面の高さを表現
    • RenderTextureから1F前の状態を取得、Greenに保存

  • これによりRenderTextureのRedには1F前、Greenには2F前の水面の高さが入っていることになり、ここから次の状態を計算することが出来る。ただし、シミュレーションスペースはQuad内に限定されており有限なので、適切な境界条件を設定する必要がある。境界条件としては状況によって以下の3パターンを使い分ける:

    • ノイマン境界条件 境界での微分量を指定:u(x+pixel) = u(x) @ x:境界
    • ディリクレ境界条件 境界での値を指定:u(x) = 0.0 @ x:境界
    • 周期的境界条件 境界は反対側の辺につながる:u(x+pixel) = u(pixel) @x:境界

  • プールなど閉鎖された空間でのシミュレーションでは、ノイマン境界条件を使うと、境界で波が反射される様子を記述できる。ディリクレ境界条件だと反射がないので、水がこぼれていくときや、反射しないドロっとしたものなどの表現に良さそうです。

2.水面パート

  • Unityの提供するSurface Shaderを利用。Surface Shaderについてはシェーダー関連の項を参照。シミュレーションで得られた水面の高さを元に、ある点を中心とした周囲4点に対する勾配を計算、そこからその点での法線の向きを求めてSurface Shaderにインプットする。
3.インタラクションパート

  • 水面の上にも水面全体を映すカメラ+レンダーテクスチャを設置する。このカメラは水面自体を移さないように水面のLayerとカメラのCulling Maskを設定しておく。そして、このカメラに映る物体がうつった際には、レンダーテクスチャを通じてシミュレーションパートに送られ、物体色の大きさ(length(col))が現在の水面の高さに加えられる。したがって、カメラに物体がうつるとその部分だけ水面が高くなり、波が周囲に広がることになる。

  • 最終更新:2018-07-05 20:52:14

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

認証パスワード