[Unity] 頂点カラーを利用して平面メッシュをテクスチャアニメーションさせる

  • 34
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめまして。
Qiita初投稿です、よろしくお願いいたします。

さて、なんだかよくわからないタイトルになってしまいましたが、
今回は『平面メッシュでのテクスチャアニメーション』を実装しようと思います。

(追記)
勉強不足でマテリアルの設定を個別に変更する方法を知らずこの記事を書いてしまいました..
頂点カラーを使わずにMaterialPropertyBlockを使ってアニメーションさせる方が楽ですので
新たに作る方は, そちらを使用してください!

3Dゲームを作成する際、見えない壁によって移動可能区域を制限する、という場面はままあると思います。
そんなとき、ただ単にコライダーにぶつかって前へすすめないというよりは
なにか壁の存在を示唆するようなエフェクトがあるほうがいいですよね。

そこで今回作ったものはこちら。

スクリーンショット 2015-08-28 9.05.33.png

スクリーンショット 2015-08-28 9.05.46.png

スクリーンショット 2015-08-28 9.07.55.png

ぶつかった場所に壁に沿ったエフェクトを表示し、そこに壁があることをわかりやすくしています。

エフェクトの表示自体はOnCollisionStay内でゴニョゴニョと書けばすぐに実装できるのですが、問題はエフェクトがアニメーションする部分です。

静止画ではわかりづらいとおもうのですが、ひとつひとつのエフェクトは以下のような連番テクスチャをオフセットずらしで表示している いわゆる「テクスチャシートアニメーション」になっています。

texture_anim_test_16.png

これを平面メッシュ上で実装したいだけなのですが....

1.ParticleSystemを使う

まずまっさきに思い浮かぶのがこれだと思います。
しかし、標準設定のままだとこのようになってしまいます。

スクリーンショット 2015-08-28 11.02.07.png

いい感じと思いきや..

スクリーンショット 2015-08-28 11.02.16.png

カメラを回転させてもエフェクトが常にカメラのほうを向いてしまう..

ふだんならありがたい機能なのですが、今回は壁の向きを表示してほしいので,固定していて欲しいですね。
ParticleSystemのRenderから表示方法を選択できるようです。

スクリーンショット 2015-08-28 11.06.13.png

通常はここがBillboardになっており、これがつねにエフェクトがこちらを向くという設定であるようです。

いろいろいじくってみましたが、近いのはありますがどれも完璧に求めているものではありません。。
[メッシュ]項目で平面を指定すればいいはずなんですが, どうもうまくいきませんでした。

2. マテリアルでアニメーションさせる

ParticleSystemがだめとなると, 少々泥臭いですがマテリアルで直にアニメーションさせることもできますね。
マテリアルの数値やテクスチャ画像を外側から定期的に変更することで、
あたかもアニメーションしているようにみせることができるというわけです!

ただ、このやり方にはひとつ決定的な弱点があります。
それは、あるオブジェクトのマテリアルの値を変更すると、同じマテリアルをもつ別のオブジェクトの外観までかわっちゃうという点です。

今回、全エフェクトが独立してアニメーションしてほしいので、この方式はとれません。。

(追記)
ウソです。できました。
MaterialPropertyBlockで実現できます。

3.頂点カラーを使ってアニメーションさせよう! ←本題

前置きが長くなってしまいました。
では、アニメーションに頂点カラーを使ってみましょうというのが今回の趣旨です。

頂点カラーとは?

メッシュ情報には [頂点の座標, 頂点の結合, 頂点に対応するUV座標]
などが含まれていますが、
実はあともうひとつ含まれているのが「頂点カラー」です。

本来頂点カラーとは、頂点に色情報をもたせて表現力を向上させようというものです。
たとえば以下の例では、うまく頂点カラーを使うことで、陰影を表現していますね。

110131b_corridor.jpg

ただ、これはライティングの技術や端末の性能が悪かった時代に重宝されたやり方で
現在ではあまりつかわれなくなっているようです。
Unityではほとんど目にしなくなったといっても過言ではないとおもいます。

この頂点カラーを着色とは別な用途に使うと便利なことが結構あります。

たとえば、UnityのTerrainのようなテクスチャブレンドも頂点カラーで簡単に実装できます。

※ テクスチャブレンドに関してはkarasuさんなどが記事を書いておられます。
http://hideapp.cocolog-nifty.com/blog/2015/04/maya-tips-maya-.html

アニメーションへの応用

今回はそんな頂点カラーをアニメーションに利用してみたいとおもいます。

全頂点のカラーを(R,G,B,A) = (0,0,0,0)からはじめて
アニメーション終了時までに(1,1,1,1)へと変化させていきます。
この(0 ~ 1)の値をシェーダー側で読み取り アニメーションの時間として利用しようというわけです。
この情報はメッシュに固有なので、たとえマテリアルを共有していてもきちんと個別にアニメーションしてくれるはずです。
※オフセット値のみ最初に頂点カラーにいれておいて、あとはシェーダー内部の時間を利用するというのも手だと思いますが、ちょっと複雑になりそうなのでこちらの手法でいきます。

C#側のコード

こんな感じです。

using UnityEngine;
using System.Collections;


public abstract class VertexColorAnimation : MonoBehaviour {
    float lifeTime;
    bool initialized;
    float startedTime;
    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {
        if ( initialized ){
            var mesh = GetComponent<MeshFilter>().sharedMesh;
            var p = (Time.time - startedTime)/lifeTime;
            var b = (byte)((p*255.0f).RoundToInt());
            var alpha = (byte)((1.0f - Barracuda.Easing.EaseOut( p , p , 0 , 1 , lifeTime)) * 255.0f ).RoundToInt();

            if (p <= 1){
                var colors = new Color32[mesh.vertexCount];
                for (int i = 0; i<mesh.vertexCount ; i++){
                    colors[i] = new Color32(b,b,b,alpha);
                }
                mesh.colors32 = colors;
            }else{
                Destroy(gameObject);
            }
        }
    }

    protected void MakeMesh ( float lifeTime ){
        this.initialized = true;
        this.lifeTime = lifeTime;
        this.startedTime = Time.time;
        var mesh = new Mesh();

        mesh.vertices = GetVertexes();
        mesh.uv = GetUVs ();
        mesh.triangles = GetTriangles();
        mesh.colors32 = new Color32[mesh.vertices.Length];
        mesh.RecalculateNormals();
        var f = gameObject.AddComponent<MeshFilter>();
        f.sharedMesh = mesh;
        gameObject.AddComponent<MeshRenderer>();
    }
    protected abstract Vector3[] GetVertexes ();
    protected abstract Vector2[] GetUVs ();
    protected abstract int[] GetTriangles ();
}


public class AnimatedPlane : VertexColorAnimation {
    // Use this for initialization
    Vector3 center;
    float size;
    Vector3 up;
    Vector3 right;


    public static AnimatedPlane CreatePlane (Vector3 position , float size , Vector3 up , Vector3 right, float lifeTime){
        var p = new GameObject();
        p.name = "AnimatedPlane";
        var a = p.AddComponent<AnimatedPlane>();
        a.center = position;
        a.size = size;
        a.up = up.normalized;
        a.right = right.normalized;
        a.MakeMesh(lifeTime);
        return a;
    }

    protected override int[] GetTriangles ()
    {
        return new int[] {
            0, 1, 2,
            3, 1, 0
        };
    }
    protected override Vector3[] GetVertexes ()
    {
        return new Vector3[] {
            center + up * size / 2 + right * size / 2 , 
            center - up * size / 2 - right * size / 2 , 
            center - up * size / 2 + right * size / 2 , 
            center + up * size / 2 - right * size / 2 
        };
    }
    protected override Vector2[] GetUVs ()
    {
        return new Vector2[] {
            new Vector2(1.0f, 1.0f),
            new Vector2(0.0f, 0.0f),
            new Vector2(0.0f, 1.0f),
            new Vector2(1.0f, 0.0f)
        };
    }
}

一応、平面以外の形状もあり得るかもということで継承させておきました。
また、今回は同時にフェードアウトもさせたかったのでRGBをテクスチャアニメーションの時間,
alpha値は不透明度として別個に利用しています。
※alpha値指定の部分にイージング関数が入ってますが単にイーズアウトさせているだけです。

シェーダーの設定(ShaderForge)

シェーダーの設定にはShaderForgeを使います。
このくらいのシェーダーであればShaderForgeがなくてもなんとか書けますが
特にデバッグがすごくやりやすいのでShaderForgeを使いました。

こんな感じです。

スクリーンショット 2015-08-28 11.25.19.png
なんかやたら巨大なマップですが
やっていることは単純です。

Column numではテクスチャシートの一列の個数をプロパティとして受け取っています。
今回は4x4の16フレームアニメーションなので4が入力されますね。

アニメーション時間をVertexColorのRで受け取り, そこから現在のIndexを算出します。
そのIndexをIndexXとIndexYに変換し、さらにテクスチャ上の2D座標に変換しています。

また、先ほどC#のコードにもあったようにalpha値を不透明度として扱いたいので
最終テクスチャに(1-alpha)分だけ黒をブレンドしていきます。
※ブレンディングはaddictive(スクリーン)にしてあるので, ここでは 黒=透明 です。

できあがったシェーダー(.shader)

ShaderForgeのコンパイルが終わると.shaderファイルが自動生成されます。
一応無駄が無いかコードもみておきましょう。

Shader "Shader Forge/TextureAnimation" {
    Properties {
        _MainTexture ("Main Texture", 2D) = "white" {}
        _Columnsnum ("Columns num", Float ) = 3
    }
    SubShader {
        Tags {
            "IgnoreProjector"="True"
            "Queue"="Transparent"
            "RenderType"="Transparent"
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode"="ForwardBase"
            }
            Blend One One
            Cull Off
            ZWrite Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #define UNITY_PASS_FORWARDBASE
            #include "UnityCG.cginc"
            #pragma multi_compile_fwdbase
            #pragma exclude_renderers gles3 metal d3d11_9x xbox360 xboxone ps3 ps4 psp2 
            #pragma target 3.0
            uniform sampler2D _MainTexture; uniform float4 _MainTexture_ST;
            uniform float _Columnsnum;
            struct VertexInput {
                float4 vertex : POSITION;
                float2 texcoord0 : TEXCOORD0;
                float4 vertexColor : COLOR;
            };
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float4 vertexColor : COLOR;
            };
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.uv0 = v.texcoord0;
                o.vertexColor = v.vertexColor;
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                return o;
            }
            float4 frag(VertexOutput i) : COLOR {
/////// Vectors:
////// Lighting:
////// Emissive:
                float node_6687 = floor((i.vertexColor.r*(_Columnsnum*_Columnsnum))); // Index
                float node_9437 = floor((node_6687/_Columnsnum)); // Y Index
                float2 node_8789 = (((1.0 / _Columnsnum)*i.uv0)+((float2(0,1)*(((_Columnsnum-1.0)-node_9437)/_Columnsnum))+(float2(1,0)*((node_6687-(node_9437*_Columnsnum))/_Columnsnum))));
                float4 _MainTexture_var = tex2D(_MainTexture,TRANSFORM_TEX(node_8789, _MainTexture));
                float3 emissive = lerp(float3(0,0,0),_MainTexture_var.rgb,i.vertexColor.a);
                float3 finalColor = emissive;
                return fixed4(finalColor,1);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
    CustomEditor "ShaderForgeMaterialInspector"
}

うむ。
複雑なマップな割にはなかなかしっかりと最適化されています!
※肝となる処理はほとんどfloat4 frag(VertexOutput i)内にありますね。

4.完成!!

ついに完成です!
わかりにくいですがしっかりとアニメーションしていますね〜

もっといい方法がありそうですが...笑

話は逸れますがShaderForgeは本当に優れたアセットだと思います。
ShaderForgeの良さを語りだすとまた脱線してしまうのでここには書きませんが
持っていない人は是非購入を検討してみてください!
※たしか6000円くらいだったと思います。

ちなみに、レントゲンのようにオブジェクトを透ける↓こんなシェーダーも作りました。

スクリーンショット 2015-08-28 11.31.28.png

需要があればまた書こうと思います。

書きました。

おしまい