見出し画像

【Unity】ParticleSystemとLineRendererの組み合わせ

こんにちは、Unityエンジニアのオオブチです。
 以前、こちらの記事ではAudioClipに反応して絵が変わるオーディオスペクトラムを作りました。今回も何か絵が華やかになるモノを作ってみたい気分なので、今回の記事ではParticleを触ろうと思います。今回はParticleSystemとLineRendererを組み合わせてこれを作ります。

 Sci-fi系の背景とかで見たことありませんか?私はあります。名前が分からないので「sci-fi 背景 点と線」みたいな言葉でググると画像が見つかったりしますが、一部ではこれの事をPlexusと呼んだりするようです。背景が何か味気ない気がするな~~って時にこういうの一個置くだけでも雰囲気上がる気がしますね。今回はこれをなるべく少ない労力で実装してみようと思います。

1. ParticleSystemの設定

まずは点になる部分をParticleSystemで作っていきます。

これ

基本的にRenderModeがBillboard、RenderAlignmentがViewになっていれば自分の好み通りに作って全然OKです。
私の今回の設定で言えば
・ShapeをBoxに
・NoiseをPositionAmountとSizeAmountにかける
・SizeOverLifetimeでCurveを作成して最初と最後を0に
みたいな設定にしました。

2. 線を引く部分

続いて線になる部分をスクリプトで引いていきます。各々のParticleの位置を繋ぐようにそれぞれLineRendererに位置を放り込んでいきます。

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

public class Plexus2 : MonoBehaviour
{
    #region variables
    [SerializeField, Tooltip("増しすぎ注意")] private int maxParticleNum = 50;
    [SerializeField] private float searchDistUnit = .13f;
    [SerializeField] private int maxLineCnt = 128;
    [SerializeField] private int maxLinesForEach = 4;
    [SerializeField] private float lineWidthDefault = .5f;
    [SerializeField] private float lineWidthMax = .5f;
    [SerializeField] private Color lineColor = Color.white;
    [SerializeField] private Material lineMaterial;
    [SerializeField] private ParticleSystem PS;
    ParticleSystem.MainModule pMainMod;
    ParticleSystem.Particle[] particles;

    private GameObject lineObj;
    private LineRenderer lineRdr;
    private List<LineRenderer> lineList;

    int idx, particleCnt;
    float searchDist;

    #endregion

    private void OnEnable()
    {
        lineList = new List<LineRenderer>();
        searchDist = searchDistUnit * PS.shape.scale.magnitude;
        pMainMod = PS.main;
        pMainMod.maxParticles = maxParticleNum;
        pMainMod.simulationSpace = ParticleSystemSimulationSpace.World;
        particles = new ParticleSystem.Particle[maxParticleNum];
        //LineRendererの設定
        lineObj = new GameObject("LineObj");
        lineObj.transform.SetParent(this.transform);
        lineRdr = lineObj.AddComponent<LineRenderer>();
        lineRdr.startColor = lineColor;
        lineRdr.endColor = lineColor;
        lineRdr.startWidth = lineWidthDefault;
        lineRdr.endWidth = lineWidthDefault;
        lineRdr.material = lineMaterial;
        lineRdr.alignment = LineAlignment.View;
        lineRdr.textureMode = LineTextureMode.Stretch;
        lineObj.SetActive(false);
    }

    private void Update()
    {
        if (PS.gameObject.activeSelf)
        {
            PS.GetParticles(particles);
            particleCnt = PS.particleCount;
            idx = 0;

            //O(n^2)
            for (int i = 0; i < particleCnt; i++)
            {
                int currentLine = 0;
                if (idx >= maxLineCnt) break;

                for (int j = i + 1; j < particleCnt; j++)
                {
                    if (currentLine >= maxLinesForEach || idx >= maxLineCnt) break;
                    ParticleSystem.Particle p1 = particles[i], p2 = particles[j];
                    float pDist = (p1.position - p2.position).magnitude;

                    if (pDist < searchDist)
                    {
                        //リストとオンオフの更新
                        LineRenderer line;
                        if (idx >= lineList.Count)
                        {
                            line = Instantiate(lineRdr, PS.transform);
                            lineList.Add(line);
                        }
                        else
                        {
                            line = lineList[idx];
                            line.gameObject.SetActive(true);
                        }
                        //位置合わせなど線の設定
                        float dist = (p1.position - p2.position).magnitude;
                        float cnst = Mathf.Clamp01(searchDist - dist) / dist * lineWidthDefault;
                        line.startWidth = Mathf.Clamp(p1.GetCurrentSize(PS) * cnst, 0, lineWidthMax);
                        line.endWidth = Mathf.Clamp(p2.GetCurrentSize(PS) * cnst, 0, lineWidthMax);
                        line.SetPosition(0, p1.position);
                        line.SetPosition(1, p2.position);
                        idx++;
                        currentLine++;
                    }
                }
            }
            for (int i = idx; i < lineList.Count; i++) lineList[i].gameObject.SetActive(false);
        }
        else if (lineList.Count >= 1)
        {
            var child = PS.transform.GetChild(0).gameObject;

            lineList.Remove(child.GetComponent<LineRenderer>());
            Destroy(child);

        }
    }
}

このスクリプトではParticleSystemの各々のParticle同士の距離を調べて、近い者同士を結ぶ線をLineRendererで引いています。maxLineCntを上限にLineRendererをInstantiateしたりオンオフしたりしてLineRendererの数を管理しています。ParticleがStopActionでDisableされたりするとLineRendererが順番に消されていきます。
 Enptyを作成して、つくったスクリプトをコンポーネントにしてParticleを子にします。適当に参照をつっけて実行してみます。

ちゃんと点と線の位置が合った、やったね

無事動きました。多分これが一番シンプルな実装なんじゃないかと思います。使う時はParticleの数を増やしすぎない事に気を付けてください。
 以上、短いですがParticle単品で使うだけじゃなくScriptとの組み合わせでこういうこともできますよ、という紹介でした。日本語でこういう説明している人は多くない気がするので参考になればいいかなと思います。
 他の実装方法としてはCustomRenderTextureとGeometryShaderでメッシュを制御している例を見たことがあります。

 ちなみにVFXGraphで作ったParticleでも同様の実装を試してみましたが、Particle個々の位置などを含むAttributeMapの類は私の扱っていたバージョンではグラフからしか触る手段が準備されていないようです、探してもそれらしい方法が見つかりませんでした。どういうことだってばよ
私スクリプトから触れますよという魔術師の方いらっしゃいましたら是非教えてください。さようなら。

3.追記

LineRendererと同様にMeshの頂点の位置を合わせると線だけではなく面で埋めたりすることもできます。コレはコレであり?


この記事が気に入ったらサポートをしてみませんか?