#Unityでのライン描画
Unityで3D空間にデバッグ用などの簡易的な線を描画する際、GLを使うことが多いかと思われますが、このGLクラスを使うにはOnRenderObjectメソッド内限定であったり、複数カメラがあると重複して描画されたりと簡易的に使うには若干扱いづらい点があります。
Debug.DrawLineなど、Debugクラスにもライン描画機能はありますが、こちらはSceneビューでしか表示されず、実機では扱えません。
そこで、Debug.DrawLineやDebug.DrawRayのように簡単に扱えるものを、GLを使って再現してみました。
##要件
- Debug.DrawLineのように簡単に扱える
- Updateメソッド内から呼び出せる
- Gameビューにも表示できる
#確認環境
Unity 2019.2.0f1
IL2CPP
Android
#実装
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
[DefaultExecutionOrder(-1)]
public class DebugLineDrawer : MonoBehaviour
{
#region Constants
#if UNITY_EDITOR
private static readonly string PREVIEW_CAMERA_NAME = "Preview Camera";
#endif
#endregion
#region Inner Class
private class LineInfo
{
public Vector3[] Points { get; set; }
public Color LineColor { get; set; }
public Camera RenderCamera { get; set; }
public int LayerMask { get; set; } = -1;
}
#endregion
#region Fields
private Material _lineMaterial;
private List<LineInfo> _lineList = new List<LineInfo>();
private Color _lineColor = Color.white;
#if UNITY_EDITOR
private List<Camera> _selectedCamera = new List<Camera>();
#endif
#endregion
#region Singleton
private static DebugLineDrawer _instance = null;
private static DebugLineDrawer Instance
{
get
{
if (_instance == null)
{
GameObject gameObj = new GameObject
{
name = typeof(DebugLineDrawer).Name
};
gameObj.AddComponent<DebugLineDrawer>();
}
return _instance;
}
}
#endregion
#region Unity Methods
void Awake()
{
// Singleton
if (_instance == null)
{
_instance = this;
DontDestroyOnLoad(gameObject);
}
else if (_instance != this)
{
Destroy(gameObject);
return;
}
// material
CreateLineMaterial();
}
void Start()
{
}
void Update()
{
_lineList.Clear();
_lineColor = Color.white;
ExtractionSelectedCameraInHierarchy();
}
void OnRenderObject()
{
_lineMaterial.SetPass(0);
GL.PushMatrix();
var currentCamera = Camera.current;
ChangePreviewCameraToSelectionCamera(ref currentCamera);
foreach (var line in _lineList)
{
#if UNITY_EDITOR
// Sceneビューには必ず映す
if (currentCamera.cameraType != CameraType.SceneView)
#endif
{
// It is not a camera for drawing this line.
if (line.RenderCamera != null && currentCamera != line.RenderCamera)
{
continue;
}
// The current camera does not draw this layer.
if ((currentCamera.cullingMask & line.LayerMask) == 0)
{
continue;
}
}
GL.Begin(GL.LINES);
GL.Color(line.LineColor);
foreach (var p in line.Points)
{
GL.Vertex3(p.x, p.y, p.z);
}
GL.End();
}
GL.PopMatrix();
}
#endregion
#region Methods
/// <summary>
/// マテリアル生成
/// </summary>
private void CreateLineMaterial()
{
if (!_lineMaterial)
{
// Unity has a built-in shader that is useful for drawing
// simple colored things.
Shader shader = Shader.Find("Hidden/Internal-Colored");
_lineMaterial = new Material(shader);
_lineMaterial.hideFlags = HideFlags.HideAndDontSave;
// Turn on alpha blending
_lineMaterial.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
_lineMaterial.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
// Turn backface culling off
_lineMaterial.SetInt("_Cull", (int)UnityEngine.Rendering.CullMode.Off);
// Turn off depth writes
_lineMaterial.SetInt("_ZWrite", 0);
}
}
/// <summary>
/// Hierarchyにて選択中のCameraを抽出
/// </summary>
[Conditional("UNITY_EDITOR")]
private void ExtractionSelectedCameraInHierarchy()
{
#if UNITY_EDITOR
_selectedCamera.Clear();
foreach (var selectedGO in UnityEditor.Selection.gameObjects)
{
var camera = selectedGO.GetComponent<Camera>();
if (camera != null)
{
_selectedCamera.Add(camera);
}
}
#endif
}
/// <summary>
/// currentCameraがPreviewCameraの場合、Hierarchy上で選択中のカメラで置き換える
/// </summary>
[Conditional("UNITY_EDITOR")]
private void ChangePreviewCameraToSelectionCamera(ref Camera currentCamera)
{
#if UNITY_EDITOR
// Preview Cameraの場合、選択中のカメラの中にRenderCameraと一致するカメラがあるかチェックする
// Camera.current.cameraType == CameraType.Previewではチェックできない(PreviewCameraもCameraType.Gameになる)ので名前で判定
if (currentCamera.name == PREVIEW_CAMERA_NAME)
{
foreach (var camera in _selectedCamera)
{
// 位置、角度、マスクなどの情報から一致するカメラを推測
// 推測制度を高めたい場合は一致させるデータを増やす
if (currentCamera.transform.position == camera.transform.position &&
currentCamera.transform.rotation == camera.transform.rotation &&
currentCamera.cullingMask == camera.cullingMask)
{
currentCamera = camera;
break;
}
}
}
#endif
}
#endregion
#region API
/// <summary>
/// 色指定無しの場合の線の色を変更する
/// </summary>
/// <param name="color"></param>
public static void SetColor(Color color)
{
Instance._lineColor = color;
}
public static void DrawLine(Vector3 startPos, Vector3 endPos, Color? color = null, Camera camera = null, int layerMask = -1)
{
var points = new Vector3[]
{
startPos,
endPos,
};
DrawLine(points, color, camera, layerMask);
}
public static void DrawRay(Vector3 position, Vector3 dir, Color? color = null, Camera camera = null, int layerMask = -1)
{
DrawLine(position, position + dir, color, camera, layerMask);
}
private static void DrawLine(Vector3[] points, Color? color, Camera camera, int layerMask)
{
if (points == null || points.Length < 2)
{
return;
}
var line = new LineInfo()
{
Points = points,
LineColor = color ?? Instance._lineColor,
RenderCamera = camera,
LayerMask = layerMask,
};
Instance._lineList.Add(line);
}
#endregion
}
#使い方
適当なSphereなどを2個置いて、_startObjectと_endObjectにそれぞれ突っ込んでください。
そしてカメラをX方向から見たものとZ方向から見たものなど、違う角度から見た2つのカメラを配置し、オブジェクトが重複して描画されるように見てみるとわかりやすいと思います。
using UnityEngine;
public class GLTest : MonoBehaviour
{
[SerializeField]
private GameObject _startObject = null;
[SerializeField]
private GameObject _endObject = null;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
var sPos = _startObject.transform.position;
var ePos = _endObject.transform.position;
// 色指定無し、カメラ指定有り
DebugLineDrawer.DrawLine(sPos, ePos, camera: Camera.main);
// 色指定無しの場合のカラーを変更
DebugLineDrawer.SetColor(Color.green);
// 色指定無し、カメラ指定有り
DebugLineDrawer.DrawLine(sPos + Vector3.up, ePos + Vector3.up, camera: Camera.main);
// 色指定有り、カメラ指定無し
DebugLineDrawer.DrawLine(sPos + Vector3.up * 2, ePos + Vector3.up * 2, Color.red);
// 色指定無し、カメラ指定有り
DebugLineDrawer.DrawLine(sPos + Vector3.up * 3, ePos + Vector3.up * 3, camera: Camera.main);
// 色指定有り、カメラ指定無し、Mask指定有り
DebugLineDrawer.DrawLine(sPos + Vector3.up * 4, ePos + Vector3.up * 4, Color.cyan, layerMask: LayerMask.GetMask("UI"));
// 色指定無し、カメラ指定無し
DebugLineDrawer.DrawRay(sPos, Vector3.up * 2);
// 色指定有り、カメラ指定無し
DebugLineDrawer.DrawRay(sPos + Vector3.up * 2, Vector3.up * 2, Color.yellow);
}
}
#解説
###基本構想
GLを利用するにはOnRenderObjectが必要なため、専用のGameObjectをSingletonで動作するようにしています。
ただし、外部からはDebug.DrawLineのようにstaticメソッドでアクセスさせたいため、Instanceはprivateにし、外部からは扱えないようにしています。
後はAPIメソッドで指定されたLine情報をListに保持していき、OnRenderObjectのタイミングで一気に描画しています。
###処理順序
Updateにて各種リセット処理を行っているため、[DefaultExecutionOrder(-1)]を指定して実行順序未定義のクラスよりも先にUpdateが処理されるようにします。
処理順序がさらに若いクラスでLine描画を行いたい場合はそのクラスよりさらに数値を低くする必要があります。
###APIメソッド
DrawLine、DrawRay共に、position以外はデフォルト引数を指定して省略できるようにしています。
使い方のところにもありますが、個別指定したい場合は「引数名:引数」の形で指定したいものだけを選択できるので大量の引数パターンをオーバーロードするよりはスッキリするかと思います。
オーバーロードの方が分かりやすいんだという方はこんな感じで実装すれば良いかと。
(中身省略)
public static void DrawLine(Vector3 startPos, Vector3 endPos) { }
public static void DrawLine(Vector3 startPos, Vector3 endPos, Color color) { }
public static void DrawLine(Vector3 startPos, Vector3 endPos, Camera camera) { }
public static void DrawLine(Vector3 startPos, Vector3 endPos, int layerMask) { }
public static void DrawLine(Vector3 startPos, Vector3 endPos, Color color, Camera camera) { }
public static void DrawLine(Vector3 startPos, Vector3 endPos, Color color, int layerMask) { }
public static void DrawLine(Vector3 startPos, Vector3 endPos, Color color, Camera camera, int layerMask) { }
でも面倒ですよね…?
####カメラの指定
cameraを指定すると、そのカメラ以外には映らなくなります。
####レイヤーマスクの指定
layerMaskを指定すると、そのMaskとカメラのCullingMaskとをAND演算し、結果が0になる場合は表示されなくなります。
逆に言うと、layerMaskで指定されたレイヤーのいずれかがCullingMaskに一つでも含まれている場合は表示されます。
マスクの指定についてはLayerMask.GetMask("Layer名")にて行います。
###SceneビューとPreview Cameraについて
UnityEditor上だと、Sceneビューや、HierarchyでCameraを選択したときにSceneビューの右下辺りに出てくるカメラのプレビュー画面もCameraでの描画として扱われ、その分だけOnRenderObjectが実行されます。
Sceneビューのカメラについては Camera.current.cameraType != CameraType.SceneView で判定できるのですが、Camera PreviewだけはCameraType.Previewにはなりませんでした。
Cameraのオブジェクト名が"Preview Camera"になるのでPreviewCameraだということは判定できるのですが、問題は複数選択されている場合です。
Hierarchy上で選択中のどのCameraなのか不明なのです。
そこで選択中のカメラの中から、位置、角度、CullingMaskが一致するカメラを抽出し、その一致したカメラということにして後の判定処理を行っています。
じゃないとAPIでカメラが指定されていた場合に、GameViewにはちゃんと映っているのにPreviewCameraにだけ映らないことになってしまいます。
##最後に
GL使おうとするたびに毎回使い方を忘れてしまって、そのたびに検索するのも億劫だったので改めて作ってみました。
あまり処理負荷などを考慮して作ってはいないので、Debug用途以外で実際のゲームなどに使いたい場合はもう少し工夫した方が良いかと思います。
むしろ実際のゲームにはもう少し複雑なことができるLineRendererなどが良さそうです。
#参考
https://docs.unity3d.com/ja/2017.4/ScriptReference/GL.html