【Unity】 簡単に水に近い表現を実現したい (Metaball)

1 はじめに

こんにちは、カヤックのソーシャルゲーム事業部のmadaです。この記事はカヤックUnityアドベントカレンダー2018の22日目の記事です。

「Lion Studios」が作成した「Happy Glass」というゲームを遊んでみて、液体の表現に興味を持ちました。この記事では「簡単に水に近い表現を実現したい」話をします。

IMAGE ALT TEXT HERE

Happy Glass - Lion Studios- Youtube

2 Metaball?

調べてみると水の表現に、Metaballという手法が使えそうでした。Metaballの説明をしようとすると数式が出てくるので、先に実装した結果を紹介します。2つの水滴が接近したときに水滴がひっつくような表現が、なんとなくできているのではないでしょうか。

f:id:kayac-mada:20181221151922g:plain

2DのMetaballを数式で説明すると、以下の式になります。画面上の(x,y)ピクセルを塗るときに、式の条件を満たしている場合塗る、そうでない場合塗らない、と制御します。例として2つのメタボールが存在するときの2次元の等高線グラフも作成してみました。

f:id:kayac-mada:20181221151929p:plain

f:id:kayac-mada:20181221151932p:plain

3 Metaballの実装概要

早速Metaballを実装していきましょう。実装するのは先程紹介した数式です。数式を分解し、3ステップに分けて紹介します。

3.1 ShaderでΣ内の計算

Σ内の計算を実装します。数式は以下の通りです。Shaderで実装するので、出力を画像のRGBAにする必要があり、_Scale, _Clip という調整用パラメーターを用意しています。frag関数の中で、uvの中心(0.5, 0.5)からの距離を色に変換しています。今回は後から調整しやすいようにShaderで実装しましたが、調整用パラメーターが定まれば画像データにしてしまっても良いところです。

f:id:kayac-mada:20181221151936p:plain:h80

Shader "Metaball/MetaballParticle" {
Properties
{
    _Color ("Color", Color) = (1,1,1,1)
    _Scale ("Scale", Range(0,0.05)) = 0.01
    _Cutoff ("Cutoff", Range(0,05)) = 0.01
}

SubShader
{
    Tags
    {
        "Queue"="Transparent"
        "IgnoreProjector"="True"
        "RenderType"="Transparent"
        "PreviewType"="Plane"
    }

    Cull Off
    Lighting Off
    ZWrite Off
    Blend One OneMinusSrcAlpha

    Pass
    {
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_fog

        #include "UnityCG.cginc"

        struct appdata_t
        {
            float4 vertex   : POSITION;
            float4 color    : COLOR;
            float2 texcoord : TEXCOORD0;
        };

        struct v2f
        {
            float4 vertex   : SV_POSITION;
            fixed4 color    : COLOR;
            float2 texcoord : TEXCOORD0;
        };

        sampler2D _MainTex;
        fixed4 _Color;
        fixed _Scale;
        fixed _Cutoff;

        v2f vert(appdata_t IN)
        {
                v2f OUT;
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = IN.texcoord;
                OUT.color = IN.color * _Color;
                return OUT;
        }

        fixed4 frag (v2f i) : SV_Target {
            fixed2 uv = i.texcoord - 0.5;
            fixed a = 1 / (uv.x * uv.x + uv.y * uv.y);
            a *= _Scale;
            fixed4 color = i.color * a;
            clip(color.a - _Cutoff);

            return color;
        }
     ENDCG
     }
}
}

f:id:kayac-mada:20181221151941p:plain:w300

3.2 カメラとRenderTextureでΣの実装

つぎにΣの実装を行います。3.1でΣ内の計算出力を画像にしたので、Σの実装はn個のMetaballを描写するだけです。次の処理で必要なのでCameraの出力はRenderTextureに設定しておきます。

f:id:kayac-mada:20181221151944p:plain:h80

f:id:kayac-mada:20181221151947p:plain:w300

f:id:kayac-mada:20181221151950p:plain:w300

3.3 ShaderでThresholdの実装

次にthresholdの実装を行います。実装は3.2で出力したRenderTextureを描写するShaderのfrag関数の中で、clip(color.a - _Cutoff);を呼ぶだけです。内側と境界に異なる色を設定できるように、clip関数の後に条件分岐を設定しています。

f:id:kayac-mada:20181221151953p:plain:h80

Shader "Metaball/MetaballRenderer" {
Properties
{
    _MainTex ("MainTex", 2D) = "white" {}
    _Color ("Color", Color) = (1,1,1,1)
    _Cutoff ("Cutoff", Range(0,1)) = 0.5
    _Stroke ("Storke", Range(0,1)) = 0.1
    _StrokeColor ("StrokeColor", Color) = (1,1,1,1)
}

SubShader
{
    Tags
    {
        "Queue"="Transparent"
        "IgnoreProjector"="True"
        "RenderType"="Transparent"
        "PreviewType"="Plane"
    }

    Cull Off
    Lighting Off
    ZWrite Off
    Blend One OneMinusSrcAlpha

    Pass
    {
    CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #pragma multi_compile_fog

        #include "UnityCG.cginc"

        struct appdata_t
        {
            float4 vertex   : POSITION;
            float4 color    : COLOR;
            float2 texcoord : TEXCOORD0;
        };

        struct v2f
        {
            float4 vertex   : SV_POSITION;
            fixed4 color    : COLOR;
            float2 texcoord : TEXCOORD0;
        };


        sampler2D _MainTex;
        half4 _Color;
        fixed _Cutoff;
        fixed _Stroke;
        half4 _StrokeColor;

        v2f vert(appdata_t IN)
        {
            v2f OUT;
            OUT.vertex = UnityObjectToClipPos(IN.vertex);
            OUT.texcoord = IN.texcoord;
            OUT.color = IN.color * _Color;
            return OUT;
        }

        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 color = tex2D(_MainTex, i.texcoord);
            clip(color.a - _Cutoff);
            color = color.a < _Stroke ? _StrokeColor : _Color;
            return color;
        }
    ENDCG
    }
}
}

f:id:kayac-mada:20181221151957p:plain:w300

成果物

以上で2DのMetaballのRenderingができるようになりました。ボールにColliderを付けて上から落としてみた画像を貼ります。まぁまぁ水に近い表現ができていると思います。比較的に簡単にできるのでタイトル通り、「簡単に水に近い表現が実現できた!」と言い切っておきます。

f:id:kayac-mada:20181221152001g:plain

簡単に上のシーンの構成を紹介します。

1. 「MetaBallLayer」のみを描写する「MetaBallCamera」を作成します。
2. 単体Metaball(水玉)を描写するために空のPrefabを作成します。
3. Prefabに(3.1)で紹介したShaderで描写するQuadを作成します。
4. Prefabのレイヤーを「MetaBallLayer」に設定します。
5. (3.2)で紹介したように、「MetaBallCamera」のRenderTargetにRenderTextureを設定します。
6. RenderTextureを「MainCamera」に描写するQuadを作成します。
7. ステージ(黒い棒)にColliderを付け、MainCameraで描写します。
8. 単体MetaballのPrefabを大量にInstantiateし、物理挙動に任せます。

※ Quadは UnityMenu > GameObject > 3D Object > Quad から生成できます

明日は?

明日は目からビームが出ている漫画名刺の「アファトさん」による「Compute ShaderでランタイムにSDFテクスチャーを生成するエクスペリメント」です。明日の記事もお楽しみに!