#はじめに
Unityで画面上に線を引くためには様々な手段がありますが、その中でも**GL**と言うOpenGLのイミディエイトモードと同様のコマンドを実行することができるグラフィックスライブラリがあるらしいです。
今回はそれを使って**数直線(数なし)**←は?を作ってみたのでご紹介したいと思います。
※2D上でしか検証していないので3Dでの動作は保証できません
コードはGithubにて公開しています。https://github.com/HarumaroJP/GLMarkLineDrawer
#使い方
Githubに公開している二つのスクリプトをUnityに入れて、GLMarkLineDrawer.csをGameObjectにくっつけるだけです。
設定欄はこんな感じです。
名前 | 用途 |
---|---|
Paths | Transform型で線の通る座標を指定します |
Color | 数直線の色を指定します |
Width | 線の幅を指定します |
Edge Mark Length | 外側のメモリの長さを指定します。 |
Inside Mark Length | 内側のメモリの長さを指定します。 |
Interval Count | メモリの分割数を指定します。 |
最初はVector3型で指定するようにしようかなと思ったのですが、かなり面倒になるので、Transform型を使うことにしました。
#GLについて
まず前提としてGLクラスでは、このような書き方で描画します。
//頂点マトリックスが漏れないようにするためのおまじない
GL.PushMatrix();
{
//描画開始
GL.Begin(GL.QUADS);
{
GL.Color(Color.white);
GL.Vertex3(x,y,z);
}
GL.End();
}
GL.PopMatrix();
・GL.PushMatrix() , GL.PopMatrix()
は、行列マトリックスが範囲外に漏れないようにするためのおまじないのようなものです。複雑な描画をする際には必要になります。今回は一応書いていますが、簡単な描画の場合は必要ないかも..?
・GL.Begin() , GL.End()
の間で、描画する頂点を指定することができます。また、引数でメッシュの形成方法を指定する方法ができます。「GL Primitives」で検索するといろいろな方法が出てくるので、調べてみてください。今回は、四角形を形成するように設定しています。
・GL.Color
は色を設定するためのメソッドです。このメソッドで設定して実行された後に面が描画された場合、設定した色が反映されるようになります。
・GL.Vertex3(x,y,z) or GL.Vertex(Vector3)
で頂点を設定することができます。
またブロック文で囲っている箇所がありますが、これは特に必要というわけではありません。可読性が上がるので、付けた方が良いというだけです。
#Drawerの解説
一旦コードを貼ります。
using System;
using UnityEngine;
[ExecuteInEditMode]
public class GLMarkLineDrawer : MonoBehaviour {
[SerializeField]
private GLMarkLine.LineSettings settings =
new GLMarkLine.LineSettings(new Transform[0], Color.white, 100f, 2f, 1f, 10);
private GLMarkLine line = new GLMarkLine(); //ラインのインスタンスを生成
private void OnRenderObject() {
line.Draw(settings); //描画
}
}
こちらの方はあまり説明は必要ないと思いますが、
一応補足をしておくと、MonobehaviourのOnRenderObject()
はカメラがシーンをレンダリングした後に呼び出されるメソッドで、今回はこのメソッドを通して描画しています。
また設定のパラメータは構造体で管理しています。あまり構造体で自前のコンストラクタは使いたくなかったのですが、入れたらすぐ使えるようにしたかったので、今回は自前のコンストラクタで初期化しています。
#本体の解説
次が本体のコードです。
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;
public class GLMarkLine {
//設定パラメータ用のstruct
[Serializable]
public struct LineSettings {
public Transform[] paths;
public Color color;
public float width;
public float edgeMarkLength;
public float insideMarkLength;
public int intervalCount;
public LineSettings(Transform[] paths,
Color color,
float width,
float edgeMarkLength,
float insideMarkLength,
int intervalCount) {
this.paths = paths;
this.color = color;
this.width = width;
this.edgeMarkLength = edgeMarkLength;
this.insideMarkLength = insideMarkLength;
this.intervalCount = intervalCount;
}
}
public LineSettings settings;
private LineSettings _settings;
private IReadOnlyList<Vector3> _paths = new List<Vector3>();
private List<Vector3> currentPaths = new List<Vector3>();
private Material lineMaterial;
private float relativeWidth;
public bool enabled = true;
private static readonly int SrcBlend = Shader.PropertyToID("_SrcBlend");
private static readonly int DstBlend = Shader.PropertyToID("_DstBlend");
private static readonly int Cull = Shader.PropertyToID("_Cull");
private static readonly int ZWrite = Shader.PropertyToID("_ZWrite");
//描画するために使うマテリアルの初期化
private void InitMaterial() {
if (!lineMaterial) {
lineMaterial = new Material(Shader.Find("Hidden/Internal-Colored"));
lineMaterial.hideFlags = HideFlags.HideAndDontSave;
lineMaterial.SetInt(SrcBlend, (int) BlendMode.SrcAlpha);
lineMaterial.SetInt(DstBlend, (int) BlendMode.OneMinusSrcAlpha);
lineMaterial.SetInt(Cull, (int) CullMode.Off);
lineMaterial.SetInt(ZWrite, 0);
}
}
public void Draw(LineSettings currentSetting) {
settings = currentSetting;
//座標が入っていなかったら描画しない
if (!enabled || settings.paths.Length <= 0 || settings.paths.Any(t => t == null)) return;
IReadOnlyList<Vector3> vecPaths = settings.paths.Select(t => t.position).ToList();
InitMaterial();
lineMaterial.SetPass(0);
GL.PushMatrix();
{
GL.Begin(GL.QUADS);
{
GL.Color(settings.color);
//設定が変更されていなかったら計算しない
if (_settings.Equals(settings) && _paths.SequenceEqual(vecPaths)) {
DotVertexes(currentPaths.ToArray());
}
else {
currentPaths.Clear();
_settings = settings;
_paths = vecPaths;
Vector3 v0, v1, o;
//解像度に対する幅を求める
relativeWidth = 1.0f / Screen.width * settings.width * 0.5f;
for (int index = 0; index < _paths.Count - 1; index++) {
v0 = _paths[index];
v1 = _paths[index + 1];
//2点の単位ベクトルを求める
o = (new Vector3(v1.y, v0.x, 0.0f) - new Vector3(v0.y, v1.x, 0.0f)).normalized;
DrawLine2D(v0, v1, o);
DrawMark2D(v0, v1, o);
}
}
}
GL.End();
}
GL.PopMatrix();
}
//2点に線を引く関数
void DrawLine2D(Vector3 v0, Vector3 v1, Vector3 o) {
Vector3 n = o * relativeWidth;
Vector3[] vertex = new[] {
new Vector3(v0.x - n.x, v0.y - n.y, 0.0f),
new Vector3(v0.x + n.x, v0.y + n.y, 0.0f),
new Vector3(v1.x + n.x, v1.y + n.y, 0.0f),
new Vector3(v1.x - n.x, v1.y - n.y, 0.0f),
};
DotVertexes(vertex);
foreach (Vector3 v in vertex) {
currentPaths.Add(v);
}
}
//2点にメモリをつける関数
void DrawMark2D(Vector3 v0, Vector3 v1, Vector3 o) {
Vector3 markLength, _v0, _v1, _o;
Vector3 _unitVec = (v1 - v0) / settings.intervalCount;
List<Vector3> _pos = new List<Vector3>();
for (int i = 0; i < settings.intervalCount + 1; i++) {
_pos.Add(v0 + _unitVec * i);
}
for (int i = 0; i < _pos.Count; i++) {
Vector3 vec = _pos[i];
float length = (i == 0 || i == _pos.Count - 1 ? settings.edgeMarkLength : settings.insideMarkLength);
markLength = o * length;
_v0 = new Vector3(vec.x - markLength.x, vec.y - markLength.y, 0.0f);
_v1 = new Vector3(vec.x + markLength.x, vec.y + markLength.y, 0.0f);
_o = (new Vector3(_v1.y, _v0.x, 0.0f) - new Vector3(_v0.y, _v1.x, 0.0f)).normalized;
DrawLine2D(_v0, _v1, _o);
}
}
//与えられた座標配列に頂点を打つ関数
void DotVertexes(Vector3[] pos) {
foreach (Vector3 v in pos) {
GL.Vertex3(v.x, v.y, v.z);
}
}
}
コードが長くなっているので、細かい説明は割愛します。
###1、数直線に幅を持たせたい
Unity側で用意されているGLクラスには、ラインの幅を設定するための方法が用意されていません。
なので、自分でメッシュの頂点座標を計算して描画する必要があります。(OpenGLの方だとあるらしい)
実装方法としては、ある直線があったとしてその直線を囲む四角形の頂点座標を算出し、描画すれば作ることができます。(要するに赤い点の座標を求めたい)
ここからは少し、数学の話になります。
下のテキストを見てください。
ここでは、上の図のような座標があったとして、まず頂点との差分(緑の線の部分)を求めます。
左のページでは、二つの頂点座標から垂直なベクトルを求めています。しかし、これだけだと二つの頂点の距離によって緑の線の長さも変わってしまいます。
そこで長さを1に固定した単位ベクトルを求めることで、それに適当な幅をかけると狙い通りの長さにすることができます。
そして最後にその差分を、元の頂点座標に足し合わせることで4点が求まります。
この部分の実装はこのようになっています。
//垂直なベクトルを求め、正規化する
o = (new Vector3(v1.y, v0.x, 0.0f) - new Vector3(v0.y, v1.x, 0.0f)).normalized;
void DrawLine2D(Vector3 v0, Vector3 v1, Vector3 o) {
//単位ベクトルに設定された幅をかける
Vector3 n = o * relativeWidth;
//オフセットを足して、頂点座標を求める
Vector3[] vertex = new[] {
new Vector3(v0.x - n.x, v0.y - n.y, 0.0f),
new Vector3(v0.x + n.x, v0.y + n.y, 0.0f),
new Vector3(v1.x + n.x, v1.y + n.y, 0.0f),
new Vector3(v1.x - n.x, v1.y - n.y, 0.0f),
};
//描画する(自作関数)
DotVertexes(vertex);
}
###2、数直線にメモリを付けたい
お気づきの方もいるかもしれませんが、1番の方法を利用すれば簡単に作ることができます。
先ほどの4点をそれぞれv0,v1とし、新たにまた4点を求めるだけです。
#まとめ
これを実装するのに約半日潰してしまった...
多分LineRendererよりは軽くなっている気がする(気がするだけ)。
正直線を引くだけならLineRendererで良い気がします。たくさん線を引きたいのであれば、GLの方が高速かも?
たくさん機能を追加すれば、もっと利便性は上がると思います。でも僕はやりません()