チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

UnityでShurikenぽいビルボードの作り方

こんな感じに、スクリプトからリアルタイムに頂点を計算してビルボードを表示しようというサンプル。標準のパーティクルシステムでは描画できないエフェクト系のコンポーネントを作りたい人向け。(Unity 4.3.4.f1)

エディタ表示だろうとカメラが複数あろうと大丈夫!(だと思う)

大事なところ

OnWillRenderObject() の受信には MeshRenderer が必要

前後左右の複数のカメラから一斉に狙われても大丈夫な板ポリゴンをレンダするには、カメラごとにOnWillRenderObject()を呼んでもらって、その都度カメラにメッシュを向けてやればオッケーとなっています。
Unity マニュアル / Unity Manual>Advanced>イベント関数の実行順
で、OnWillRenderObjectの受信にはどうやらMeshRendererコンポーネントの登録が必要。MeshRendererは1つのGameObjectにつき1つまで。MeshRendererの取り合いを避けるため、隠し子GameObjectを作ります。今回は、エディタに表出する設定保持コンポーネントA&隠し子オブジェクト側でレンダリングとメッシュを管理するコンポーネントBの二台体制でいきます。

Billboard.cs
using UnityEngine;

namespace BillboardSystem
{
    [ExecuteInEditMode()]
    public class Billboard : MonoBehaviour
    {
        /// <summary>レンダリング時に使用するマテリアル</summary>
        public Material material;

        private GameObject renderObject;
        void OnEnable()
        {
            if (renderObject == null)
            {
                renderObject = new GameObject("hideChild for Billboard Renderer");
                renderObject.hideFlags = HideFlags.HideAndDontSave;
                renderObject.transform.parent = transform;
                renderObject.transform.localScale = Vector3.one;
                renderObject.transform.localPosition = Vector3.zero;
                renderObject.transform.localRotation = Quaternion.identity;
                renderObject.AddComponent<BillboardRenderer>().ownerBillboard = this;
            }
        }
    }
}

有効化された時に隠し子オブジェクトの有無をチェックして無ければ作ります。その時、後述のBillboardRendererコンポーネントを追加して、自身への参照を渡します。

で、下記がそのBillboardRendererコンポーネントの実装例。

BillboardRenderer.cs
using UnityEngine;

namespace BillboardSystem
{
    [ExecuteInEditMode()]
    public class BillboardRenderer : MonoBehaviour
    {
        /// <summary>親ビルボードを取得、設定します。</summary>
        public Billboard ownerBillboard;

        private Mesh mesh;
        private MeshFilter meshFilter;
        private MeshRenderer meshRenderer;

        void OnEnable()
        {
            if (gameObject == null) return;

            if (mesh == null)
            {
                mesh = new Mesh();
                mesh.name = "RuntimeMesh";
                mesh.hideFlags = HideFlags.HideAndDontSave;
            }

            if (meshFilter == null)
            {
                meshFilter = gameObject.AddComponent<MeshFilter>();
                meshFilter.sharedMesh = mesh;
            }

            if (meshRenderer == null) meshRenderer = gameObject.AddComponent<MeshRenderer>();
        }
        void OnDisable() { if (mesh != null) mesh.Clear(); }
        void OnDestroy() { if (Application.isEditor) DestroyImmediate(mesh); else Destroy(mesh); }

        void OnWillRenderObject()
        {
            mesh.Clear();

            if ((Camera.current == null) || (Camera.current.transform == null)) return;
            
            // カメラ平面にX,Y軸として映る単位ワールドディレクションを取得
            Vector3 meshHorizontalDirection = Camera.current.transform.TransformDirection(Vector3.right);
            Vector3 meshVerticalDirection = Camera.current.transform.TransformDirection(Vector3.up);

            if (ownerBillboard != null) meshRenderer.material = ownerBillboard.material;
            
            // オブジェクトの中心から、ビルボードの端点を決定します。基本的にワールド座標における1のサイズですが、lossyScaleのx,yもオマケします。
            Matrix4x4 worldToLocalMatrix = transform.worldToLocalMatrix;
            Vector2 lossyScale = transform.lossyScale;
            Vector3 vH = worldToLocalMatrix * (meshHorizontalDirection * (0.5f * lossyScale.x));
            Vector3 vV = worldToLocalMatrix * (meshVerticalDirection * (0.5f * lossyScale.y));
            Vector3[] vertices = new Vector3[] {
                -vH - vV, //左下
                -vH + vV, //左上
                vH + vV, //右上
                vH - vV, //右下
            };
            mesh.vertices = vertices;
            mesh.colors32 = new Color32[] { Color.white, Color.white, Color.white, Color.white };

            mesh.uv = new Vector2[] {
                new Vector2(0, 0), //左下
                new Vector2(0, 1), //左上
                new Vector2(1, 1), //右上
                new Vector2(1, 0), //右下
            };

            int[] tris = new int[] { 0, 1, 2, 2, 3, 0 };

            mesh.subMeshCount = 1;
            mesh.SetTriangles(tris, 0);
            mesh.RecalculateNormals();
        }
    }
}

お借りした素材

  • FreeAsset "Dead Tower Scene" (3D Models/Enviroments/Fantasy)
  • 写研 BA-90

でもシーンロードで変なログが出る。

ヨシヨシデキタと思ったら、こんなログががが

CheckConsistency: Transform child can't be loaded

ちょっと調べてみると3.xから放置されているGameObject.hideFlagにDontSaveを設定すると生じるバグの模様。…次の日記で対応版書く。