読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

オブジェクト表面を垂れる液体表現 その2

C# Graphics Shader Unity UnityTexturePaint

はじめに

f:id:es_program:20161208132108g:plain:w600

ペイント後に垂らす処理

f:id:es_program:20161208125410g:plain:w600

ロゴの表示演出(© UTJ/UCL)

f:id:es_program:20161208125332g:plain:w600

複数色のテスト

esprog.hatenablog.com

オブジェクト表面を液体が垂れる表現をさせる試みの続きです。

前回の記事で基本的な部分を実装したのですが、複数の色をペイントすることが出来ないという問題がありました。 複数色置けなかった理由は、白黒で液体の付着量を表すペイントマップを参照して色付けしていたことが原因です。

今回はペイントマップに書き込むデータを変更して複数色対応したよというお話です。

液体を垂らす処理の手順

文章での説明だとかなり冗長になる・・・ リポジトリは以下で公開しているのでそっち参考にした方がいいかもしれません。

github.com

前回と大枠は変わらないのですが、順に説明していきます。

  • オブジェクトのメインテクスチャ、ノーマルマップ、ハイトマップをそれぞれコピーしたRenderTextureを用意する

    これは、UnityTexturePaintのDynamicCanvasでUse...項目をチェックすれば自動で行われます。 f:id:es_program:20161208133156p:plain:w600

  • 用意したRenderTextureを描画に利用するため、Materialのテクスチャと差し替える

    f:id:es_program:20161208125249p:plain:w600 上画像のようなイメージです。 これもDynamicCanvas付けてれば勝手にやってくれます。

  • UnityTexturePaintでペイントマップ(先程ハイトマップをコピーしたRenderTexture)のRGBに塗りたい色を、Aに高さを格納する(ハイトマップに塗っている理由は、"今回は"ハイトマップ=ペイントマップとして利用するため)

    f:id:es_program:20161208134455p:plain:w600 塗りに利用するブラシの設定で「Color RGB Height A」を選びます。これでブラシのカラー情報がテクスチャのRBGに、高さ情報がAに書き込まれます。

  • ペイントマップのA(高さのデータで、オブジェクト表面の液体の付着量を表すことになる)を参照してノーマルマップを生成する

    esprog.hatenablog.com

    基本的には上記記事そのままです。参照する情報をペイントマップのAに変更するだけ。 シェーダーは以下のような感じ

Shader "Unlit/HeightToNormal"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _bumpMap("Default BumpMap", 2D) = "white" {}
        _NormalScaleFactor("NormalScale", FLOAT) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _BumpMap;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;
            float _NormalScaleFactor;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            float4 PackNormal(float3 normal) {
#if defined(UNITY_NO_DXT5nm)
                return float4(normal);
#else
                return float4(normal.y, normal.y, normal.y, normal.x);
#endif
            }

            float4 frag (v2f i) : SV_Target
            {
                float2 shiftX = { _MainTex_TexelSize.x, 0 };
                float2 shiftZ = { 0, _MainTex_TexelSize.y };

                float4 texX = 2 * tex2Dlod(_MainTex, float4(i.uv.xy + shiftX, 0, 0)) - 1;
                float4 texx = 2 * tex2Dlod(_MainTex, float4(i.uv.xy - shiftX, 0, 0)) - 1;
                float4 texZ = 2 * tex2Dlod(_MainTex, float4(i.uv.xy + shiftZ, 0, 0)) - 1;
                float4 texz = 2 * tex2Dlod(_MainTex, float4(i.uv.xy - shiftZ, 0, 0)) - 1;

                float3 du = { 1, 0, _NormalScaleFactor * (texX.a - texx.a) };
                float3 dv = { 0, 1, _NormalScaleFactor * (texZ.a - texz.a)};


                float3 normal = normalize(cross(du, dv));

                float4 tex = tex2Dlod(_MainTex, float4(i.uv.xy, 0, 0));
                if (tex.a <= 0)
                    return tex2Dlod(_BumpMap, float4(i.uv.xy, 0, 0));

                return PackNormal(normal * 0.5 + 0.5);
            }
            ENDCG
        }
    }
}

で、これを使ってOnWillRenderObjectでBlitしてやれば、法線が更新されていきます。

   private void OnWillRenderObject()
    {
        var canvas = GetComponent<DynamicCanvas>();
        var materialName = canvas.GetComponent<Renderer>().sharedMaterial.name;

        //HeightMapからNormalMap生成
        var normalPaint = canvas.GetPaintNormalTexture(materialName);
        var normalTmp = RenderTexture.GetTemporary(normalPaint.width, normalPaint.height);
        height2Normal.SetTexture("_BumpMap", normalPaint);
        height2Normal.SetFloat("_NormalScaleFactor", normalScaleFactor);
        Graphics.Blit(heightPaint, normalTmp, height2Normal);
        Graphics.Blit(normalTmp, normalPaint);
        RenderTexture.ReleaseTemporary(normalTmp);
    }

height2Normalが先程のシェーダーを適用したマテリアルです。

  • ペイントマップのRGBをメインテクスチャに書き込む

    ペイントマップのRGBをそのままメインテクスチャに書き込みます。 シェーダーは以下の様な感じでシンプルです。

Shader "Unlit/HeightToColor"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ColorMap("ColorMap", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            sampler2D _ColorMap;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float4 mainCol = tex2D(_MainTex, i.uv);

                if (mainCol.a > 0) {
                    return float4(mainCol.rgb, 1);
                }

                return tex2D(_ColorMap, i.uv);
            }
            ENDCG
        }
    }
}

で、これを使ってOnWillRenderObjectでBlitしてやります。

   private void OnWillRenderObject()
    {
        var canvas = GetComponent<DynamicCanvas>();
        var materialName = canvas.GetComponent<Renderer>().sharedMaterial.name;

        //HeightMapからMainTexture生成
        var mainPaint = canvas.GetPaintMainTexture(materialName);
        var mainTmp = RenderTexture.GetTemporary(mainPaint.width, mainPaint.height);
        height2Color.SetTexture("_ColorMap", mainPaint);
        Graphics.Blit(heightPaint, mainTmp, height2Color);
        Graphics.Blit(mainTmp, mainPaint);
        RenderTexture.ReleaseTemporary(mainTmp);
    }

height2Colorが先程のシェーダーを適用したマテリアルです。

  • ペイントマップを指定した方向に広げる

    ペイントマップをダラーっと垂らす部分です。 この部分がまだ色々未完成で試行錯誤しています(まだ色々問題あり)。 仮組みですが、シェーダーは以下の様な感じ。

Shader "Unlit/HeightSimpleStretch"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ScaleFactor ("ScaleFactor", FLOAT) = 1
        _Viscosity("Viscosity", FLOAT) = 0.1
        _FlowDirection("Flow Direction", VECTOR) = (0, 0, 0, 0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;
            float _ScaleFactor;
            float _Viscosity;
            float4 _FlowDirection;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                float4 col = tex2D(_MainTex, i.uv);

                //TODO:VITIATEにちっちゃいノイズ入れたい。
                const float VITIATE_X = 0.3;//どのくらい横の液体を考慮するかの係数。
                float2 shiftZ = float2(_FlowDirection.x * _MainTex_TexelSize.x, _FlowDirection.y * _MainTex_TexelSize.y) * _ScaleFactor * _Viscosity;
                float2 shiftX = float2(_MainTex_TexelSize.x * _FlowDirection.y, _MainTex_TexelSize.y * _FlowDirection.x) * _ScaleFactor * _Viscosity * VITIATE_X;
                float2 shiftz = -shiftZ;
                float2 shiftx = -shiftX;

                //TODO:直下の高さを取ってきて、その高さに応じてどの程度流れるかを決めたい
                float4 texZ = tex2Dlod(_MainTex, float4(clamp(i.uv.xy + shiftZ, 0, 1), 0, 0));
                float4 texx = tex2Dlod(_MainTex, float4(clamp(i.uv.xy + shiftx + shiftZ, 0, 1), 0, 0));
                float4 texX = tex2Dlod(_MainTex, float4(clamp(i.uv.xy + shiftX + shiftZ, 0, 1), 0, 0));

                //ピクセルの液体付着量を計算
                float amountUp = texZ.a * 0.5 + texx.a * 0.25 + texX.a * 0.25;//上にある液体の付着量(重みは直上優先)

                //上のピクセルが塗られていた場合、垂れてくると仮定して加算
                if (amountUp > 0) {
                    //垂れてきた液体を加算した合計の液体付着量
                    float resultAmount = (col.a + amountUp) * 0.5;
                    //垂れた液体の色を計算
                    float3 maxRGB = max(col.rgb, max(texZ.rgb, max(texx.rgb, texX.rgb)));//全ての範囲の色の最大値(これで計算すると全体的に明るくなるのでまずい)
                    float3 resultRGB = lerp(maxRGB, texZ.rgb, clamp(amountUp - _Viscosity, 0, 1));//垂れてくる液体との線形補間

                    return float4(resultRGB, resultAmount);
                }

                //上のピクセルが塗られていない場合、今現在参照しているピクセルの色をそのまま帰す
                return col;
            }
            ENDCG
        }
    }
}

で、これを使ってOnWillRenderObjectでBlitしてやります。

   private void OnWillRenderObject()
    {
        var canvas = GetComponent<DynamicCanvas>();
        var materialName = canvas.GetComponent<Renderer>().sharedMaterial.name;

        //HeightMapを垂らす
        var heightPaint = canvas.GetPaintHeightTexture(materialName);
        var heightTmp = RenderTexture.GetTemporary(heightPaint.width, heightPaint.height);
        heightFluid.SetFloat("_ScaleFactor", flowingForce);
        heightFluid.SetFloat("_Viscosity", viscosity);
        heightFluid.SetVector("_FlowDirection", flowDirection.normalized);
        Graphics.Blit(heightPaint, heightTmp, heightFluid);
        Graphics.Blit(heightTmp, heightPaint);
        RenderTexture.ReleaseTemporary(heightTmp);
    }

heightFluidが先程のシェーダーを適用したマテリアルです。

といった感じになります。

オブジェクト表面を垂れる液体表現 その1

Unity UnityTexturePaint Graphics

はじめに

採用情報:仕事を読み解くキーワード24 - 23 もう一歩踏み込みたいより

Splatoon動画の壁に塗ったインクが垂れるような表現がしたかったので作ってますというお話です。

まだまだ垂れる部分のアルゴリズムを試作中だったりしますが、ある程度形になってきたので現状報告。

f:id:es_program:20161127200034g:plain:w600

液体の法線データも変化していくため、わりと液体が垂れてる感が出てるかと思います。 壁に使っているシェーダーはいつも通りUnityデフォルトのStandard Shaderです。

液体を垂らす方法

前提として、UnityTexturePaintを使っていきます。

いくつか方法があると思いますが、自分が思いついた2つの方法について見ていきます。

液体の垂れを球で近似していく

見えない球(MeshRendererのenabledをfalseにしたもの)を転がして、球が当たった場所にペイントを重ねていくというかなり単純な方法です。

f:id:es_program:20161127203055g:plain:w600

水色の球が液体の垂れる動作を表現していて、球が転がった先が液体の垂れる場所だと仮定して塗っていきます。 この水色の球をレンダリングしないようにするとこんな感じに。

f:id:es_program:20161127203955g:plain:w600

実装自体は既存機能使うだけでノーコーディングなので楽なのですが、処理が重いのと、高さや法線の情報が塗る度に上書きされるだけなので、垂れてる感がイマイチ出てません。

ペイントマップから高さや法線が垂れる部分を計算して書き換える

塗りを行った場所に、インクの高さ情報を持つテクスチャ(ペイントマップ)を作成し、そのデータをもとに画像処理的に「インクの広がり」、「高さ情報の変化」、「法線情報の変化」を計算する方法です。

f:id:es_program:20161127205204p:plain:w600

まず、上記画像のような、塗った場所に白黒でインクの高さ情報を格納するペイントマップを用意します。 このテクスチャは、ハイトマップとして利用することが出来ます。しなくてもいいですが。

ペイントマップに対して垂れる方向をUV座標で与え、高さの情報をもとに画像処理的にインクを広げていきます。

f:id:es_program:20161127205953p:plain

インクを広げるアルゴリズム

冒頭で言及したとおり、垂れる部分のアルゴリズムは試作中です。現在のアルゴリズムは、各ピクセルに対して垂れてくる液体の量を高さから計算して平均を取る単純なものです。 このアルゴリズムで重要になるのは、「質量保存の法則」をある程度守ることで、これをしないとインクの総量が増えたり減ったりするので違和感を覚えることになったりします(言い換えれば、インクの収束条件に配慮する)。 具体的には、処理前と処理後でインクの高さ情報の総量がなるべく変化しないように心掛けます。 例えば、インクの総量が増え続けるアルゴリズムの場合、垂れていくインクが収束せず、ずっと垂れ続けるなんてことになります。

次に、ペイントマップの高さ情報に対して、法線情報を計算し、テクスチャに格納します。

高さの情報から法線を得る方法は以下の記事で。

esprog.hatenablog.com

法線情報を可視化すると、こんな感じになります。

f:id:es_program:20161127211735g:plain:w600

最後に、ペイントマップを参照して、塗った場所(高さが0より上の場所)に色を付けるようにすれば完成です。

f:id:es_program:20161127200057g:plain:w600

オブジェクトの反射と併用するとちょっとおもしろい表現が出来たりします。

f:id:es_program:20161127200104g:plain:w600

さいごに

まだまだ簡単に作っただけで試作段階なので、より液体が垂れてるっぽくするためにどうしたらいいのか考えていこうと思います。