しゅみぷろ

プログラミングとか

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

はじめに

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が先程のシェーダーを適用したマテリアルです。

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