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

凹みTips

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

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

C# Unity

はじめに

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 ビルドしたものが動かないですね。。スレッド切ってるからかな。