• モザイクがらみのシェーダを作る

    2018-05-13 00:33
    ■経緯

     Unityの開発がらみで生放送で何かやろうと思っても、Skyrimの女の子を使ってたりすると、うっかりあられもない姿が出てしまうので、怖くてできない。
     当初は FBX に体も服も同梱してたので割りと安全だったのだが、システム的に汚いと思い、分離させた。
     結果、初期状態はハダカで、Awake/Start時点で衣服を着る、という設計になったので、プレビューをとめた時点でもうアウト。ますますあかんくなった。

     そうこうしてるうちに、今の興味はまたゲーム作りのほうにシフトしているのだが、去年来考えていた「ゲーム未満」とは別に、まさしく「ゲーム」を作りたくなってきた。アイディアで難局を切り抜けていくやーつ。ディスオナみたいに、攻略方法がバラエティに富むやーつ。

     そこで、ゲーム内容に関わる要素として、オブジェクトにモザイクをかけたくなった。通常だとモザイクなしで露出状態になるが、あるアイテムを使うとモザイクをかけることができる、というギミックに使う予定。
     逆なんじゃね? アイテムで外すんじゃね? ってところだけど、いいの。あってるの。実際のところ、ゲーム性に深く関わることではない。おまけ要素に近い。でも、この作品をどう位置づけたいか、というところで、とても大事な要素。

    ■これまで使ってたモザイク

     あられもない姿防止のために、一応 Ist/MosaicField というのを見つけて、使わせていただいていた。以下のような仕組みである。

    モザイクをかけたい部分にオブジェクトをかぶせる

    ポリゴン空間基準で色を塗るのではなく、
    スクリーン空間基準でフレームバッファから取った色を塗る
    (背景を基にしたエフェクトを行なう)

    できあがり

     だが、気になる点が2つ。1つは、SSAO を使うと、モザイク用オブジェクトにも影がついてしまうこと。まあ、それはたいしたことではない。
     もう1つは、オブジェクトの輪郭が出てしまい、なんとなくガラス球を持っているように見えてしまうこと。前景があって一部が隠れるとなおさらだ。

    粒子を粗くするとより「球感」が目立つ

     単なる好みの問題で、これだってたいしたことは無いのだが、技術的に乗り越えたくなるじゃあないか。

     上記例では ZTest を Less にしてある。だから手前の脚に隠れてしまう。
     じゃあ ZTest を Always にしたらいいじゃないか、と思えば、それも違う。それだと、右手が完全に体に隠れていても、背中側から見えてしまう。

    Always だと、隠さなくていい何かを隠してしまう

     具体的な要望は2つ。
      ・1つの球や1つの円ではなく、複数の矩形の集合にしたい(エッジから球感をなくす)
      ・Less や LEqual を仮定して、矩形の一部でも見えようものなら、
       Always で矩形全部を描画する。そうでなければけっして描画しない。

     前者はともかくとして、後者は面倒だ。言わば、複数ピクセルの ZTest の結果を論理演算で結んだあとで、それらのピクセルすべてに適用する、という処理だ。そのままそれをやらないにせよ、簡単には実現できそうにない。(ただし、できるけど俺が知らないだけ、って可能性がおおいにあるけどな)

    ■でもモザイクじゃないものを作ることになりました

     で、それをあーだこーだ考えているうちに、「モザイクもいいけど、せっかくだから別形式も考えてみよう」と思い始めた。そこで、目線だとか、ハートマークだとか、「見せられないよ!」なんかを表示するものを作ってみることにした。

    こういう感じにしてみたい

     ワールド空間上の平面ではなく、スクリーン空間上の平面(かのようなもの)、つまりビルボードにする。これだとそれぞれの部位ごとに1つの矩形で済むから、上述の1つめの要望は満たされる。

     問題は2つめ。ZTest をうまくやれるかどうか。

    やれました

     右胸のハートマークは隠蔽に成功。
     左胸や目線は、陰になって既に隠れてはいるのだが、「ビルボードの中央または4隅が1点でも見えていれば全部表示する」という仕様にしているので、表示されてしまう。
     また、目線は本来なら斜めに入れたいところだが、それにも対応してない。

     などなど、まだまだ改善の余地があるが、おおむねできあがった。

    ■ソース:スクリプト側

     まず、自前で ZTest を行なうため、どこかのスクリプトで以下を行なっておく。
    Camera.main.depthTextureMode |= DepthTextureMode.Depth;
     これで、シェーダプログラムがデプスバッファにアクセスできるようになる。

     次に、ビルボードを適用したいボディメッシュの SkinnedMeshRenderer に、今回作った Material を追加する。
     ポイントは、モザイクをかける部分を Armature モデル内での空間位置ではなく、ボディメッシュのテクスチャ UV空間上の平面位置で指定すること。

    右手中指の付け根は 618,783(テクスチャサイズは2048×1024)

    Texture2D tex = Resources.Load("BillboardTexture") as Texture2D;
    Material m = new Material(Shader.Find("Custom/HeartMosaic5"));
    m.SetTexture("_MainTex", tex);
    m.SetVector("_TargetPoint0", new Vector4(618 / 2048f, 1 - 783 / 1024f, 0.12f, 1));

    GameObject handObj = GameObject.Find("Shape_Hands_0");
    SkinnedMeshRenderer smr = handObj.GetComponent<SkinnedMeshRenderer>();
    List<Material> ms = new List<Material>();
    ms.AddRange(smr.sharedMaterials);
    ms.Add(m);
    smr.sharedMaterials = ms.ToArray();
     ひとつの Material につき、_TargetPoint1~3 を含めた4ヶ所を指定できる。
     Vector4 の x,y が UV。z はサイズ(ワールド空間のm単位)、w はテクスチャに乗算するアルファ値。

     ちなみに "HeartMosaic5" がなんで 5 なのかというと、試行段階の 1~4 でいろんな失敗を経たから。最終的にモザイクですらなくなっちゃった。

    ■ソース:シェーダ側

     パフォーマンスなどでいろいろアヤシイところもあるので、今のところ、全文掲載は控えておく。重要なポイントだけ。
     
     まず、ジオメトリシェーダを定義する。ポリゴンを受け取り、UV等々に関する検査を経て、合格すればビルボードを返す。
    [maxvertexcount(4)]
    void geom(triangle v2g input[3], inout TriangleStream<g2f> outStream) {
    checkAndCreate(_TargetPoint0, input, outStream);
    //略。以下同
    }
     checkAndCreate は以下のとおり。
    void checkAndCreate(float4 target, v2g input[3], inout TriangleStream<g2f> outStream) {
    //演算回数を減らすための簡易検査処理(略)

    //UV空間ポリゴンに指定UVが内包されているかの検査
    float f0 = forCheckPolygon(target.xy, input[0].uv, input[2].uv);
    float f1 = forCheckPolygon(target.xy, input[1].uv, input[0].uv);
    float f2 = forCheckPolygon(target.xy, input[2].uv, input[1].uv);
    if (f0 > 0 || f1 > 0 || f2 > 0) {
    return;
    }

    //カメラ方向やビルボード変換の事前計算
    float3 cameraDir =
    float3(-UNITY_MATRIX_V._m02, -UNITY_MATRIX_V._m12, -UNITY_MATRIX_V._m22);
    float4x4 billboardMatrix = UNITY_MATRIX_V;
    billboardMatrix._m03 = billboardMatrix._m13 = billboardMatrix._m23 =
    billboardMatrix._m33 = 0;

    //ポリゴン中心点
    float4 polygonCenterModel =
    (input[0].vertex + input[1].vertex + input[2].vertex)/3;
    float4 polygonCenterWorld = mul(unity_ObjectToWorld, polygonCenterModel);

    //表示は Always だが、ビルボード実物は対象点の5cm手前に存在するように仮定する
    float4 polygonCenterProj =
    mul(UNITY_MATRIX_VP, polygonCenterWorld - float4(cameraDir*0.05, 0));

    //ビルボード頂点情報の配列。
    //potentially uninitialized って言われるので、適当に初期化しておく
    g2f o;
    o.vertex = polygonCenterWorld;
    o.uv = float2(0, 0);
    o.color = float4(0, 0, 0, 0);
    g2f outputs[4] = { o, o, o, o };

    //自前 ZTest
    bool hidden = checkDepth(cameraDir, polygonCenterProj);
    for (int x = 0; x <= 1; x++) {
    for (int y = 0; y <= 1; y++) {
    //ビルボードの各頂点を計算
    float4 billboardCornerWorld = polygonCenterWorld +
    mul(float4((x-0.5) * target.z, (y-0.5) * target.z, 0, 1), billboardMatrix);
    float4 billboardCornerProj =
    mul(UNITY_MATRIX_VP, billboardCornerWorld - float4(cameraDir*0.05, 0));

    //自前 ZTest
    hidden = hidden && checkDepth(cameraDir, billboardCornerProj);

    //ビルボード頂点情報の決定
    g2f output;
    output.vertex = billboardCornerProj;
    output.uv = float2(x, y);
    output.color = float4(1, 1, 1, target.w);
    outputs[y + x*2] = output;
    }
    }

    //中央・四隅とも隠れていたら生成とりやめ
    if (hidden) {
    return;
    }

    //トライアングルストリップ追加
    outStream.Append(outputs[0]);
    outStream.Append(outputs[1]);
    outStream.Append(outputs[2]);
    outStream.Append(outputs[3]);
    outStream.RestartStrip();
    }
     ボディメッシュのポリゴンよりも 5cm手前に配置しているのは、四隅と中央がめり込んでしまいかねないため。ただ、それでも、ビルボードが大きすぎたりすると、うまくいかないこともある。
     
    こういうときとか。古葉監督みたいに片側ならいいんだけど

     ボディメッシュのほとんど全部のポリゴンについて描画不要なので、その分、このあたりの検査精度をあげることにコストを割いてもいいかもしれない。
     ついでに、フレームバッファを使った、従来型のモザイクを作ってもいいかも知れないね。ビルボードを10x10分割してそれぞれ検査する、みたいな。

     forCheckPolygon では xy平面に沿った3次元ベクトル同士の外積を取って、zの値を返す。首尾よくいけば全部負になる(・・・ということらしい。間違ってたらごめんね)。ここなどが参考になりました。
    float forCheckPolygon(float2 T, float2 uvA, float2 uvB) {
    float3 vec1 = float3(uvA - uvB, 0);
    float3 vec2 = float3(T - uvB, 0);
    return cross(vec1, vec2).z;
    }
     一番大事な、自前での zTest の部分。解説はコメントで入れておきます。
    bool checkDepth(float3 cameraDir, float4 billboardCornerProj) {
    //まず、ビルボードの角の座標を、プロジェクター空間における値から
    //スクリーン空間の値に変換する
    float4 screenPosTmp = ComputeScreenPos(billboardCornerProj);
    float2 screenUV = screenPosTmp.xy / screenPosTmp.w;

    //sample2D _CameraDepthTexture 経由で、デプスバッファにアクセスできる。
    //各ピクセル値はエンコードされてるらしい。
    //デコードするには、マクロを使う。
    //カメラの near~far間のうち、どの地点にいるか、という値らしい。
    //1で near、0 で far。
    //なお、geom下で用いるため、tex2D ではなく tex2Dlod を使う。
    float textureDepth =
    UNITY_SAMPLE_DEPTH(tex2Dlod(_CameraDepthTexture, float4(screenUV.xy, 0, 0)));

    //プロジェクター空間における座標値の z は、w で割ることでデプスに対応するらしい
    return (billboardCornerProj.z/ billboardCornerProj.w <= textureDepth);
    }
     ここまで、Ist/MosaicField のソースのほか、いろんなサイトが参考になったのだが、どれもこれも断片的だったので、何処とは一概に言えない。モデル空間、ビュー空間、プロジェクター空間、スクリーン空間が頭の中でごっちゃになって、あーでもないこーでもない、という試行錯誤の時間のほうがずっと長かった。GPUは裏取り、デバッグがとっても大変ね・・・。

     フラグメントシェーダ関数は以下のみ。この記事が参考になるような方には説明不要ですな。
    float4 frag(g2f i) : SV_Target {
    return tex2D(_MainTex, i.uv) * i.color;
    }

     結局ほとんど全文載せちゃったな。
  • 広告
  • トゥーンな水流 開発中

    2018-04-17 07:01


     もう作り出してから(作るのに一旦飽きてから)1年くらい経つゲーム未満、トゥーン調にするのかリアル調にするのかまったく決めかねている中、トゥーンっぽい水流ができかけているので、これを川に使いたいなぁートゥーンにしようっかなーなんて。

     良くある表現で、多分いずこでも実践されていることなんだけど、いろいろ勉強してきたことがまとまって形になりそうなのが楽しい。
     

     まだ全然完成じゃないんだけどな。

     1.Unity標準のパーティクルを配置。ビルボードで透明~半透明~不透明の球(ビルボードなので円だけど)を描く
     2.レイヤーで隔離して、専用カメラで撮影
     3.ポストエフェクトで、2Dメタボール映像に変換
       (半透明同士の円が重なりあって、一定以上の透明度になったところのみを採用、という感じ)
     4.レンダリングされた内容を Canvas, RawImage でオーバレイ描画

     とここまではいいのだが、問題点が。

     右の崖のところ、水が透けちゃってるのね。2.でパーティクルだけ撮影してるから、陰線処理もクソもないわけ。
     ステンシルバッファか何か使えば解決できそうなんだけど、回りくどくなりそうでな。もう、テクスチャアニメーションでいいんじゃなかろうか・・・。でもそれじゃありきたりすぎて面白くないよねぇ。

    <追記>

     録画にはこちらを使わせていただいた。重宝してます。

     http://www.f-sp.com/entry/2016/09/06/032801

     なお、これだと録画できるのはカメラ映像だけで、オーバレイの uGUI の内容は映せないっぽいんで、今回は ScreenSpace - Camera / distance 短くして強引に撮った。

     前までは oCam 使ってたんだけど、取り込み位置調整が面倒で困ってたし、仮想通貨のマイニングボットを仕込みやがったんで(毎度アプデ拒否してたんで大丈夫だったはずなんだけど、寝ぼけて押したのかもしれない)、さようなら。

    <追記>



     陰線処理もできた。

     ・ParticleSystem のレイヤーを独立させる。
     ・パーティクルレイヤー以外を映すカメラAの深度をー1とする。
     ・パーティクルレイヤーのみを映すカメラBの深度をー2とする。
      ・カメラBの前方100mの位置に、真っ黒な Quad を配置する。
      ・カメラBのクリア条件を Don't Clear にする。

     こうすることで Z-Buffer が維持されるのでパーティクルが陰線処理されつつ描画される。できれば、Z-Buffer のみ残して、色情報は float4(0, 0, 0, 0) でクリアしてほしかったのだが、そんなことしてくれないらしい。なので Quad を配置したのだが、もっとスマートな方法もありそう。

     んで、それがメタボールに変換されると以下のような画像になる。あとはこれをカメラAにオーバレイ表示すればいい。
     


     輪郭線も入れてみたけど、まあ、いまひとつですな。








  • 誰得マップ:獣血の丸薬マラソン用聖杯ダンジョン97zvbb6v

    2018-04-10 03:27
    未だにやってるブラッドボーン。2ヶ月のブランクを経て、初期レベゼロゼロあと3ボス。

     マップ作りも楽しい。つい作ってしまった。

    作った理由:獣血の主を倒すために、以前見つけておいたダンジョンでマラソン敢行。
    成果:10+98ヶになった。
    結果:80ヶ以上余った。

     まあ、黒獣でも使うさ・・・多分。



     誰得の上塗りで解説
     基本路線:獣向けの炎武器を用意すること。
     ・一層:レバーと宝箱は死に戻り前提で突っ込むこと。
         このダンジョンでアメンドーズの次に難しいのは多分最初のレバー。
     ・二層:ここらの当番はどっちも銀獣なのでイージー。
         番人は壁にハメて殺しましょう。
     ・三層:正面の部屋に丸薬が計6ヶ。
         下段にしかないので、上段に座ってる銀獣は無視しておk。
         宝箱方面には、螺旋階段の一番頂上に2ヶあり。
         アメンドーズは無視して結構。時間の割に益無し。

     マップに省略した箇所も全部拾ったと思うけど、せいぜい酒とか落とし子とか死血とか。雷光ヤスリ部屋もあった気もするけど、ボス撃破の遺志で大分買えるからねぇ。

     宝箱の上位者の叡智(初回はカレル文字かも)、ボス撃破での啓蒙、落ちてる智慧も合わせて、計21ヶ分。
     最速20分でいけた。縛り無しならも少し早いと思う。