凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity のトゥーンシェーダについて調べてみた

はじめに

自前のトゥーンシェーダを作りたくて、まずは Unity 付属のトゥーンシェーダについて調べてみました。Unity のシェーダについては以前の記事をサラッと読んでいただけると理解しやすいと思います。

環境

  • Unity 5.0.1f1

Toon/Basic

トゥーンシェーダは Assets > Import Package > Effects アセットについてきます。まずは一番シンプルな Toon/Basic を見てみます。

f:id:hecomi:20150425144236p:plain

コードを見てみます。

Shader "Toon/Basic" {
    Properties {
        _Color ("Main Color", Color) = (.5,.5,.5,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _ToonShade ("ToonShader Cubemap(RGB)", CUBE) = "" { }
    }


    SubShader {
        Tags { "RenderType"="Opaque" }
        Pass {
            Name "BASE"
            Cull Off
            
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #pragma multi_compile_fog

           #include "UnityCG.cginc"

            sampler2D _MainTex;
            samplerCUBE _ToonShade;
            float4 _MainTex_ST;
            float4 _Color;

            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };
            
            struct v2f {
                float4 pos : SV_POSITION;
                float2 texcoord : TEXCOORD0;
                float3 cubenormal : TEXCOORD1;
                UNITY_FOG_COORDS(2)
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.cubenormal = mul (UNITY_MATRIX_MV, float4(v.normal,0));
                UNITY_TRANSFER_FOG(o,o.pos);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _Color * tex2D(_MainTex, i.texcoord);
                fixed4 cube = texCUBE(_ToonShade, i.cubenormal);
                fixed4 c = fixed4(2.0f * cube.rgb * col.rgb, col.a);
                UNITY_APPLY_FOG(i.fogCoord, c);
                return c;
            }
            ENDCG           
        }
    } 

    Fallback "VertexLit"
}

とてもシンプルなコードです。何をやっているかというと、シンプルな頂点 / フラグメントシェーダに以下の様なキューブマップテクスチャを入力として与えている形になります。

f:id:hecomi:20150425144830p:plain

上記は標準の Effects アセットをインポートした時についてくるテクスチャです。Unity 5 からはキューブマップテクスチャとして入力された画像のアスペクト比を見て、インポート時に自動的にキューブマップテクスチャにしてくれます。上記は 128x128 と 1:1 なテクスチャなのでスフィアマップなテクスチャと認識されます。

スフィアマップとして扱われるので、中心の色が上側、周辺の色が下側として描画されて良い感じに影ができるわけです。Toon/Basic は計算量的には軽量ですが、ライトの位置に関係なく下側に影が描画される形になります(試しにディレクショナルライトをグリグリしてみても影の位置は変わりません)。

ちなみに本エントリの主題からは外れますが、#pragma multi_compile_fogUNITY_FOG_COORDSUNITY_TRANSFER_FOG は Unity 5 から Vertex / Fragment シェーダへ Fog の効果を追加する便利機能のようです。

Toon/Lit

ライトの影響を受けるトゥーンシェーダとして Toon/Lit が用意されています。

f:id:hecomi:20150425172923p:plain

Toon Ramp というプロパティに以下の様なテクスチャを与えています。

f:id:hecomi:20150425173027p:plain

コードを見てみます。

Shader "Toon/Lit" {
    Properties {
        _Color ("Main Color", Color) = (0.5,0.5,0.5,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Ramp ("Toon Ramp (RGB)", 2D) = "gray" {} 
    }

    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
CGPROGRAM
#pragma surface surf ToonRamp

sampler2D _Ramp;

// custom lighting function that uses a texture ramp based
// on angle between light direction and normal
#pragma lighting ToonRamp exclude_path:prepass
inline half4 LightingToonRamp (SurfaceOutput s, half3 lightDir, half atten)
{
   #ifndef USING_DIRECTIONAL_LIGHT
    lightDir = normalize(lightDir);
   #endif
    
    half d = dot (s.Normal, lightDir)*0.5 + 0.5;
    half3 ramp = tex2D (_Ramp, float2(d,d)).rgb;
    
    half4 c;
    c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
    c.a = 0;
    return c;
}


sampler2D _MainTex;
float4 _Color;

struct Input {
    float2 uv_MainTex : TEXCOORD0;
};

void surf (Input IN, inout SurfaceOutput o) {
    half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
ENDCG

    } 

    Fallback "Diffuse"
}

Surface Shaderカスタムライティングモデルを利用した実装になっています。

Surface Shader は、表面の材質を記述する Surface Function と、ライティングとの相互作用を記述する Lighting Model からなるシェーダです。通常は Lighting Model は組み込みの Lambert(Diffuse)か Blingphone(Specular)を指定しますが、これらで表現できない今回のような場合に、カスタムライティングモデルを利用する形になります。カスタムライティングは Lighting* 接頭辞から始まる関数を作成し、#pragma surface でその関数を使用するライティングモデルとして指定します。

それではコードを見てみます。ミソは以下の行で、ここで光のあたっている量を計算しています。

half d = dot (s.Normal, lightDir)*0.5 + 0.5;

簡単に説明すると、面の法線方向と入射光の方向を比較し、光源方向と表面の法線方向が一致しているほど 1.0 に近づき、一致していない(逆を向いている)ほど 0.0 に近づきます。図解した詳細は以下のページが詳しいです。

そして、この値 d をテクスチャの UV 座標 (d, d) として与え、影の強さとして取り出し、これと表面の色、ライトの色、減衰率を掛けて最終的な色として採用します。

half3 ramp = tex2D (_Ramp, float2(d,d)).rgb;
c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);

なお、Forward 用に #pragma lighting で Diferred で効かないように記述されています。また、減衰率(attenuation)を示す atten が 2 倍で与えられるのは、Unity 4 系までの負債で Unity 5 からは要らないのですが、おそらく修正し忘れではないかと思います(どっかで報告できないのかな)。

ちなみに影ありのディレクショナルライトと一緒にそのまま使うと結構汚い感じになってしまいます。

f:id:hecomi:20150425173338p:plain

影を消せば綺麗にはなるのですが、影を表示したい場合の解決方法はちょっと思いつかないので保留です。。

Toon/Basic Outine

f:id:hecomi:20150425213730p:plain

Toon/Basic OutlineToon/Lit Outline がありますが、両方共 2-Pass なシェーダを利用して再現しています。

Shader "Toon/Basic Outline" {
    Properties {
        _Color ("Main Color", Color) = (.5,.5,.5,1)
        _OutlineColor ("Outline Color", Color) = (0,0,0,1)
        _Outline ("Outline width", Range (.002, 0.03)) = .005
        _MainTex ("Base (RGB)", 2D) = "white" { }
        _ToonShade ("ToonShader Cubemap(RGB)", CUBE) = "" { }
    }
    
    CGINCLUDE
   #include "UnityCG.cginc"
    
    struct appdata {
        float4 vertex : POSITION;
        float3 normal : NORMAL;
    };

    struct v2f {
        float4 pos : SV_POSITION;
        UNITY_FOG_COORDS(0)
        fixed4 color : COLOR;
    };
    
    uniform float _Outline;
    uniform float4 _OutlineColor;
    
    v2f vert(appdata v) {
        v2f o;
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

        float3 norm   = mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal);
        float2 offset = TransformViewToProjection(norm.xy);

        o.pos.xy += offset * o.pos.z * _Outline;
        o.color = _OutlineColor;
        UNITY_TRANSFER_FOG(o,o.pos);
        return o;
    }
    ENDCG

    SubShader {
        Tags { "RenderType"="Opaque" }
        UsePass "Toon/Basic/BASE"
        Pass {
            Name "OUTLINE"
            Tags { "LightMode" = "Always" }
            Cull Front
            ZWrite On
            ColorMask RGB
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #pragma multi_compile_fog
            fixed4 frag(v2f i) : SV_Target
            {
                UNITY_APPLY_FOG(i.fogCoord, i.color);
                return i.color;
            }
            ENDCG
        }
    }
    
    Fallback "Toon/Basic"
}

UsePass "Toon/Basic/BASE" で先ほどの Toon/Basic シェーダを使って描画します。そして次に2つ目の Pass でアウトラインを描画します。やっていることとしては WebGL の解説ですが、アルゴリズムは同じなので wgld.org さんの図を見ると分かりやすいと思います。

ざっくり言うと、ちょっと拡大して裏側だけ描画するようにして指定されたアウトライン色で塗りつぶす、という感じです。拡大する部分のコードを見てみます。

float3 norm   = mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal);
float2 offset = TransformViewToProjection(norm.xy);

ここの計算はちょっと複雑なのですが、要は法線の方向を求めています。v.normal は元々のモデルの法線方向ですが、ライティングするためには回転・スケーリングされた後の法線が欲しいわけです。回転は単純に回転すればよいのですが、スケーリングに関しては面倒で、例えば球をペチャンコに潰すと法線は上面は (0, 1, 0)、下面は (0, -1, 0) となり、方向が変わるのが想像できると思います。また法線の大きさも正規化しないと行けません。このあたりを良い感じに計算しているのが上記2行なわけですね。ちなみに UNITY_MATRIX_IT_MV はモデルビュー転置行列の逆行列(IT = Inverse Transposed)です。

z 方向は拡大しても描画に関係ないので、得られた xy 成分だけを offset として取り出し o.pos に加えます。

o.pos.xy += offset * o.pos.z * _Outline;

ここで、o.pos.z をかけているのは、物体を遠ざけても同じサイズのアウトラインの太さを得るためです。o.pos.z を取り除くと近づくほど太く、遠ざかるほど細くなるアウトライン(= 物体の縁をマーカで黒く塗ったような感じ)になります。

これで横と縦に膨らんだモデルが得られます。後は _OutlineColor で塗りつぶして描画すれば、めでたくアウトラインが描画できるわけです。ただし、この方法には弱点があって、法線方向に膨らめているだけなので直角に交わる面付近などで線が途切れてしまいます。

f:id:hecomi:20150425200922p:plain

Toon/Lit Outline

f:id:hecomi:20150425213848p:plain

Toon/Lit Outline では、ここで作った OUTLINEUsePass してる実装になってます。再利用出来るの良いですね。

Shader "Toon/Lit Outline" {
    Properties {
        _Color ("Main Color", Color) = (0.5,0.5,0.5,1)
        _OutlineColor ("Outline Color", Color) = (0,0,0,1)
        _Outline ("Outline width", Range (.002, 0.03)) = .005
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Ramp ("Toon Ramp (RGB)", 2D) = "gray" {} 
    }

    SubShader {
        Tags { "RenderType"="Opaque" }
        UsePass "Toon/Lit/FORWARD"
        UsePass "Toon/Basic Outline/OUTLINE"
    } 
    
    Fallback "Toon/Lit"
}

改造してみる

色々分かったところで自前でトゥーンシェーダを書いてみます。以下のページで紹介されている方法で 1 Pass でアウトラインを描画してみます。

具体的には、Toon/Lit では法線 - 光源方向の内積の値のみ使ってテクスチャから1次元で色を取り出していましたが、法線 - 視線方向の内積の値も使って2次元でテクスチャから色を取り出します。

Shader "Toon/LitWithOutline" {
    Properties {
        _Color ("Main Color", Color)  = (0.5, 0.5, 0.5, 1)
        _MainTex ("Base (RGB)", 2D)   = "white" {}
        _Ramp ("Toon Ramp (RGB)", 2D) = "gray" {} 
    }

    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
CGPROGRAM
#pragma surface surf ToonRamp

sampler2D _Ramp;
sampler2D _MainTex;
half4     _Color;

inline half4 LightingToonRamp(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
   #ifndef USING_DIRECTIONAL_LIGHT
    lightDir = normalize(lightDir);
   #endif
    
    half u = dot(s.Normal, lightDir) * 0.5 + 0.5;
    half v = dot(s.Normal, viewDir);
    half3 ramp = tex2D(_Ramp, half2(u, v)).rgb;
    
    half4 c;
    c.rgb = s.Albedo * _LightColor0.rgb * ramp;
    c.a = 0;
    return c;
}

struct Input {
    half2 uv_MainTex : TEXCOORD0;
};

void surf (Input IN, inout SurfaceOutput o) {
    half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
ENDCG

    } 

    Fallback "Diffuse"
}

カスタムライティングモデルで指定する Lighting Function では、引数を追加すれば視線方向である half3 viewDir を受け取ることができるので、これを利用しています。入力する画像は以下の様なものです。

f:id:hecomi:20150425224417p:plain

結果がコレです。

f:id:hecomi:20150425205427p:plain

...うーん、微妙ですね。

おまけ:Unity 4.x から Unity 5.x へのトゥーンシェーダの移行

これまで Unity 4.x 系でトゥーンシェーダを使っていたプロジェクトを Unity 5 に持ってくるとアウトラインが極太で変になると思います。これは以下の移行ガイドに書いてあるように、法線方向へのふくらめ度合いの計算時に normalize するように修正してみてください。

// float3 norm   = mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal);
float3 norm   = normalize(mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal));

おわりに

仕組みがわかると色々と工夫しがいがあると思いますので、みなさんも色々と工夫したトゥーンシェーダ作ってみてください。

Unity でキャラクタの足の位置を地面の形状に合わせてみた

はじめに

段差や坂を登る時などに単にアニメーションさせているだけではコライダの位置に合わせてキャラクタが上下するだけで、片方の足は地面に付いているけど、もう片方の足の位置は地面から離れているといった不自然な表現になってしまいます。

これを解決するには、アニメーションした後に地面の形状に応じて足の位置と傾きを上書きしてあげれば良いわけです。Unity では MonoBehaviourOnAnimatorIK() などのタイミングで IK(Inverse Kinematics)を利用することで、手前の関節も位置も考慮して両手両足の位置を上書きをすることが出来ます。これを利用すると簡単に足を地面に沿わせることが出来ます。前半はこれについて解説します。

地面の形状に合うようにつけるのは簡単なのですが、動いている時も自然な見た目になるようにしようとすると色々と面倒になります。自分で作るのも大変なので、有料になってしまいますが Unity の有名なアセットの一つである Final IK を使ってセットアップする方法について後半解説します。

スクリーンショット

未使用時

f:id:hecomi:20150422222028p:plain

Unity 標準の Animator IK

f:id:hecomi:20150422222001p:plain

Final IK(Grounder FBBIK)

f:id:hecomi:20150422222440p:plain

デモ

Final IK のデモです。

Unity 標準の Animator IK の利用

4.x では IK はプロ版の機能でしたが 5.x からフリー版でも使えるようになりました。

Unity では MonoBehaviour.OnAnimatorIK() および Unity 5 から導入された StateMachineBehaviour.OnStateIK() のタイミングで、IK を利用して両手両足の位置をアニメーション更新後に上書きして指定することが出来ます。

以下、地面に設置させるサンプルです。

using UnityEngine;
using System.Collections;

public class FootIK : MonoBehaviour 
{
    private Animator animator_;

    public bool  isDrawDebug = false; 
    public float heelOffsetZ = 0f;
    public float toeOffsetZ  = 0f;
    public float rayLength   = 0.2f;

    // 身体の各ボーンの位置は Animator から取れるのでエイリアスを作っておくと便利
    private Transform leftFoot  { get { return animator_.GetBoneTransform(HumanBodyBones.LeftFoot);  } }
    private Transform rightFoot { get { return animator_.GetBoneTransform(HumanBodyBones.RightFoot);  } }
    private Transform leftToe   { get { return animator_.GetBoneTransform(HumanBodyBones.LeftToes); } }
    private Transform rightToe  { get { return animator_.GetBoneTransform(HumanBodyBones.RightToes); } }


    void Start()
    {
        animator_ = GetComponent<Animator>();
    }


    void OnAnimatorIK()
    {
        // IK の位置から踵・つま先のオフセットを設定
        var heelOffset   = Vector3.up * heelOffsetZ;
        var toeOffset    = Vector3.up * toeOffsetZ;
        var leftHeelPos  = leftFoot.position  + heelOffset;
        var leftToePos   = leftToe.position   + toeOffset;
        var rightHeelPos = rightFoot.position + heelOffset;
        var rightToePos  = rightToe.position  + toeOffset;

        // 足の位置を IK に従って動かす
        var leftIkMoveLength  = UpdateFootIk(AvatarIKGoal.LeftFoot,  leftHeelPos,  leftToePos);
        var rightIkMoveLength = UpdateFootIk(AvatarIKGoal.RightFoot, rightHeelPos, rightToePos);

        // 身体の位置を下げないと IK で移動できないので
        // IK で移動させた差分だけ身体を下げる
        animator_.bodyPosition += Mathf.Max(leftIkMoveLength, ightIkMoveLength) * Vector3.down;
    }


    float UpdateFootIk(AvatarIKGoal goal, Vector3 heelPos, Vector3 toePos)
    {
        // レイを踵から飛ばす(めり込んでた時も平気なようにちょっと上にオフセットさせる)
        RaycastHit ray;
        var from   = heelPos + Vector3.up * rayLength;
        var to     = Vector3.down;
        var length = 2 * rayLength;

        if (Physics.Raycast(from, to, out ray, length)) {
            // レイが当たった場所を踵の場所にする
            var nextHeelPos = ray.point - Vector3.up * heelOffsetZ;
            var diffHeelPos = (nextHeelPos - heelPos);

            // Animator.SetIKPosition() で IK 位置を動かせるので、
            // 踵の移動分だけ動かす
            // 第1引数は AvatarIKGoal という enum(LeftFoot や RightHand など)
            animator_.SetIKPosition(goal, animator_.GetIKPosition(goal) + diffHeelPos);
            // Animator.SetIKPositionWeight() では IK のブレンド具合を指定できる
            // 本当は 1 固定じゃなくて色々フィルタ掛けると良いと思う
            animator_.SetIKPositionWeight(goal, 1f);

            // 踵からつま先の方向に接地面が上になるように向く姿勢を求めて
            // IK に反映させる
            var rot = GetFootRotation(nextHeelPos, toePos, ray.normal);
            animator_.SetIKRotation(goal, rot);
            animator_.SetIKRotationWeight(goal, 1f);

            // レイを確認用に描画しておくと分かりやすい
            if (isDrawDebug) {
                Debug.DrawLine(heelPos, ray.point, Color.red);
                Debug.DrawRay(nextHeelPos, rot * Vector3.forward, Color.blue);
            }

            return diffHeelPos.magnitude;
        }

        return 0f;
    }


    Quaternion GetFootRotation(Vector3 heelPos, Vector3 toePos, Vector3 slopeNormal)
    {
        // つま先の位置からレイを下に飛ばす
        RaycastHit ray;
        if (Physics.Raycast(toePos, Vector3.down, out ray, 2 * rayLength)) {
            if (isDrawDebug) {
                Debug.DrawLine(toePos, ray.point, Color.red);
            }
            var nextToePos = ray.point + Vector3.up * toeOffsetZ;
            // つま先方向に接地面の法線を上向きとする傾きを求める
            return Quaternion.LookRotation(nextToePos - heelPos, slopeNormal);
        }
        // レイが当たらなかったらつま先の位置はそのままで接地面方向に回転だけする
        return Quaternion.LookRotation(toePos - heelPos, slopeNormal);
    }
}

これで冒頭のスクショのようになります。

ただ、このままでは動かすと地面に近づいた時に足がアニメーションで地面につく前に IK で地面についてしまうのでビッタンビッタンした動きになってしまいます。これを防ぐために足の上下速度に応じてフィルタを掛けたりしてあげたり、StateMachineBehaviour.OnStateIK() の方で特定のモーションの時だけ接地するようにしてあげたりする必要があります。また走っていて足が垂直になるようなときに、上記スクリプトでは踵-つま先方向を保ったまま地面につけようとする結果、すごい角度で地面に接地した感じになってしまいます。これを防ぐために足の角度制限をつけてあげたりする必要があると思います。また、Animator IK は膝位置を調整できないので、やけに内股になってしまったりします。

Final IK を利用する

こういった問題点は、Final IK に同梱されている Grounder FBBIK を使うと解決します。Final IK は $90 のアセットで Full Body な IK や人以外の IK も出来たりと Unity の標準の IK と比べるとかなり高機能になっています。動画も沢山上がっているので見てると買いたくなると思います。

Grounder FBBIK はこんな感じで使えます。

基本的には Full Body Biped IK(人型の全身の IK)と Grounder FBBIK をアタッチして、Grounder FBBIKIkFull Body Biped IK をドラッグ&ドロップするだけで良いのですが、セットアップが失敗することがあります。

f:id:hecomi:20150423013639p:plain

赤や黄色のジョイントがダメなところです。このまま動かすととても残念な感じになります。

f:id:hecomi:20150423014041p:plain

原因はピンと伸びていることのようで、以下の動画の後半部のように微妙に関節を動かしてあげると解決します。

f:id:hecomi:20150423014135p:plain

とても簡単です。

おわりに

Animator 周りの知識まだまだ乏しいのですが、勉強しながら、動きが綺麗でかつ色々出来る面白いキャラクタコントローラ作りたいです。

Particle を使って Line Renderer みたいな表現を作ってみた

はじめに

Line Renderer で満足できない場面があったので Particle を密に並べて線っぽく見せるのを作ってみました。

デモ

f:id:hecomi:20150418165018g:plain

解説

マウスに追従して線を描くみたいなデモを作ってみます。マウス座標を単純に補間して出すだけだとカクカクな線になってしまうので適当に滑らかになるように補間します。綺麗な曲線にしようとすると過去の点がたくさん必要に成り遅延が大きくなってしまうので、1フレの遅延で済むようにここでは2次のスプラインの平均を取る方式で作ってみました。

using UnityEngine;
using System.Collections.Generic;

public static class LineInterpolation
{
    public static List<Vector3> GetQuadraticPoints(
            Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4, int num) 
    {
        var points = new List<Vector3>();
        for (int i = 0; i < num; ++i) {
            var t = (float)i / (num - 1);
            var l1 = GetQuadraticPoint(p1, p2, p3, 0.5f * (1f + t));
            var l2 = GetQuadraticPoint(p2, p3, p4, 0.5f * t);
            points.Add((l1 + l2) * 0.5f);
        }
        return points;
    }

    private static Vector3 GetQuadraticPoint(
            Vector3 p1, Vector3 p2, Vector3 p3, float t)
    {
        return Vector3.Lerp(Vector3.Lerp(p1, p2, t), Vector3.Lerp(p2, p3, t), t);
    }
}

最新から過去に遡って4点を与えると、2〜3の間の曲線を作ってくれる感じです。これを使って ParticleSystem.Emit() で出すものを作ります。

using UnityEngine;
using System.Collections.Generic;

public class ParticleTrail : MonoBehaviour 
{
    public ParticleSystem particleSystem;

    public int pointNum = 100;
    public float interpolateLength = 0.2f;

    private List<Vector3> points_ = new List<Vector3>();
    public List<Vector3> points
    {
        get { return points_; }
    }

    void Start()
    {
        points_.Clear();
    }

    void Update()
    {
        AddPoint(transform.position);

        if (points_.Count > 3) {
            var n = Mathf.CeilToInt((points_[1] - points_[2]).magnitude / interpolateLength);
            if (n < 2) n = 2;
            foreach (var point in LineInterpolation.GetQuadraticPoints(
                points_[0], points_[1], points_[2], points_[3], n)) {
                Emit(point);
            }
        } else {
            Emit(points_[0]);
        }
    }

    void AddPoint(Vector3 point)
    {
        if (points_.Count >= pointNum) {
            points_.RemoveAt(pointNum - 1);
        }
        points_.Insert(0, point);
    }

    void Emit(Vector3 point)
    {
        particleSystem.Emit(
            point,
            Random.onUnitSphere * particleSystem.startSpeed,
            particleSystem.startSize,
            particleSystem.startLifetime,
            particleSystem.startColor);
    }
}

ここでは transform.position の位置に出すようにしているので、マウス座標に応じて適当にオブジェクトを動かすスクリプトを書きます。

using UnityEngine;
using System.Collections;

public class MoveByMouse : MonoBehaviour 
{
    void Update() 
    {
        var mousePos = Input.mousePosition;
        mousePos.z = 10f;
        transform.position = Camera.main.ScreenToWorldPoint(mousePos);
    }
}

この2つのスクリプトを空の GameObject にアタッチするとこんな感じの表現ができます。

f:id:hecomi:20150418162121p:plain

Particle がビルボードで表示されるのでポリゴンになる Line Renderer よりどの角度で見ても綺麗だと思います。Particle System の代わりに Particle Playground を使って、 ParticlePlaygroundC.Emit() でパーティクルを出すと、もっと色々な動きのある線を描くことが出来ます。冒頭のスクショはこれになります。

おまけ: Scene View で補間点の確認

線の補間を色々と試すときに Editor スクリプトを使って Scene View に補間点がどうなるかを描画すると便利です。以下のサイトを参考に Editor スクリプトを書いてみます。 catlikecoding.com

using UnityEngine;
using System.Collections.Generic;

[CustomEditor(typeof(ParticleTrail))]
public class ParticleTrailEditor : Editor 
{
    private const float PointSize = 0.05f;
    private const float DeltaLength = 0.05f;

    private void OnSceneGUI() 
    {
        var trail = target as ParticleTrail;

        var points = trail.points;
        Handles.color = Color.gray;
        Handles.DrawPolyLine(points.ToArray());

        var interpolatedPoints = new List<Vector3>();
        for (int i = 0; i < points.Count; ++i) { 
            var sceneForward = SceneView.lastActiveSceneView.camera.transform.forward;
            Handles.color = Color.white;
            Handles.DrawSolidDisc(points[i], sceneForward, PointSize);
            if (i >= 3) {
                var n = Mathf.CeilToInt((points[i - 2] - points[i - 1]).magnitude / trail.interpolateLength);
                if (n == 0) n = 1;

                var partialPoints = LineInterpolation.GetQuadraticPoints(
                    points[i - 3], points[i - 2], points[i - 1], points[i], n);
                interpolatedPoints.AddRange(partialPoints);
            }
        }

        Handles.color = Color.green;
        Handles.DrawPolyLine(interpolatedPoints.ToArray());
    }
}

これで、オリジナルの点を結んだ時の線と、補間した点がどうなるかの差を視覚的にチェックしながら開発ができるようになります。適当な点を Add テスト用データを突っ込めるようにしておくと良いと思います(以下テストデータの例と Particle Playground 版のコード)。

using UnityEngine;
using System.Collections.Generic;
using ParticlePlayground;

public class ParticleTrail : MonoBehaviour
{ 
    public PlaygroundParticlesC particle;

    public int pointNum = 100;
    public float interpolateLength = 0.2f;

    private List<Vector3> points_ = new List<Vector3>();
    public List<Vector3> points
    {
        get { return points_; }
    }

    void Start()
    {
        points_.Clear();
    }

    void Update()
    {
        AddPoint(transform.position);

        if (points_.Count > 3) {
            var n = Mathf.CeilToInt((points_[1] - points_[2]).magnitude / interpolateLength);
            if (n < 2) n = 2;
            foreach (var point in LineInterpolation.GetQuadraticPoints(
                points_[0], points_[1], points_[2], points_[3], n)) {
                particle.Emit(point);
            }
        } else {
            particle.Emit(points_[0]);
        }
    }

    void AddPoint(Vector3 point)
    {
        if (points_.Count >= pointNum) {
            points_.RemoveAt(pointNum - 1);
        }
        points_.Insert(0, point);
    }

    [ContextMenu("Input Test Data")]
    void Test()
    {
        points_.Add(new Vector3(0, 0, 0));
        points_.Add(new Vector3(0, 1, 0));
        points_.Add(new Vector3(0, 0, 1));
        points_.Add(new Vector3(1, 1, 0));
        points_.Add(new Vector3(2, 0, 0));
        points_.Add(new Vector3(0, 2, 2));
        points_.Add(new Vector3(3, 0, 3));
    }
}

これで Scene View で確認するとこんな感じになります。

f:id:hecomi:20150418163740p:plain

おわりに

数千個単位で描画しないとならないので描画負荷的にゲーム用途にはちょっと厳しいかな、と思いますが、インタラクションデモとかには良いのではないでしょうか。

余談

Particle Playground を利用すると WebGL ビルドしたものが動かないですね。。スレッド切ってるからかな。