エディタ拡張で簡単なグラフを描画する方法に関するメモ.
背景の描画
Handles.DrawSolidRectangleWithOutline()で背景と枠線をまとめて表示できる.(※GUI.GroupScopeを使用する場合は,そちらでGUIStyleを渡して背景描画することも)
デモコード
DemoEditorWindow.cs
// デモ表示用のエディタウインドウ
public class DemoEditorWindow : EditorWindow {
private void OnGUI() {
EditorGUILayout.Space();
GUILayout.Label("-------------");
{ // グラフ描画
var rect = GUILayoutUtility.GetRect(300, 200).Padding(10);
Handles.DrawSolidRectangleWithOutline(rect, Colors.backdropColor, Colors.gridLineColor);
}
GUILayout.Label("-------------");
}
/// <summary>
/// 背景と枠線を描画する
/// </summary>
public void DrawBackDrop(Rect rect, Color backdropColor, Color outlineColor) {
Handles.DrawSolidRectangleWithOutline(rect, backdropColor, outlineColor);
}
private static class Colors {
public static readonly Color backdropColor = new Color(0.8f, 0.8f, 0.8f, 0.1f);
public static readonly Color gridLineColor = new Color(0.9f, 0.9f, 0.9f, 0.5f);
}
}
【結果】
グリッド線の描画
Handles.Line()でGUIに線を描く場合、EditorWindow左上を原点とした座標を渡す必要がある.そのためグラフ範囲のRect内に描画するように変換を行っている.
デモコード
DemoEditorWindow.cs
private void OnGUI() {
EditorGUILayout.Space();
GUILayout.Label("-------------");
{ // グラフ描画
var settings = new AxisSettings(xRange: (0, 10), yRange: (0, 10), xStep: 1, yStep: 2);
var rect = GUILayoutUtility.GetRect(300, 200).Padding(10);
DrawBackDrop(rect, Colors.backdropColor, Colors.gridLineColor);
DrawGrid(rect, settings, Colors.gridLineColor);
}
GUILayout.Label("-------------");
}
public void DrawGrid(Rect rect, AxisSettings settings, Color gridColor) {
using (new Handles.DrawingScope(gridColor)) {
var graphOrigin = rect.position;
// X軸
var xStart = Mathf.Ceil(settings.xRange.min / settings.xStep) * settings.xStep;
var xTicks = EnumerableUtil.LinspaceWithStep(xStart, settings.xRange.max, settings.xStep);
foreach (var x in xTicks) {
var value1 = new Vector2(x, settings.yRange.min);
var value2 = new Vector2(x, settings.yRange.max);
// GUI座標に変換して描画
var p1 = Convertor.ValueToGraphPoint(value1, rect.size, settings) + graphOrigin;
var p2 = Convertor.ValueToGraphPoint(value2, rect.size, settings) + graphOrigin;
Handles.DrawLine(p1, p2);
}
// Y軸
var yStart = Mathf.Ceil(settings.yRange.min / settings.yStep) * settings.yStep;
var yTicks = EnumerableUtil.LinspaceWithStep(yStart, settings.yRange.max, settings.yStep);
foreach (var y in yTicks) {
var value1 = new Vector2(settings.xRange.min, y);
var value2 = new Vector2(settings.xRange.max, y);
// GUI座標に変換して描画
var p1 = Convertor.ValueToGraphPoint(value1, rect.size, settings) + graphOrigin;
var p2 = Convertor.ValueToGraphPoint(value2, rect.size, settings) + graphOrigin;
Handles.DrawLine(p1, p2);
}
}
}
public struct AxisSettings {
// グラフの値域
public (float min, float max) xRange;
public (float min, float max) yRange;
// グリッド幅
public float xStep;
public float yStep;
public AxisSettings((float min, float max) xRange, (float min, float max) yRange, float xStep, float yStep) {
this.xRange = xRange;
this.yRange = yRange;
this.xStep = xStep;
this.yStep = yStep;
}
}
private static class Convertor {
// グラフの値域
public static (float min, float max) xRange = (0, 10);
public static (float min, float max) yRange = (0, 10);
/// <summary>
///
/// </summary>
public static Vector2 ValueToGraphPoint(Vector2 value, Vector2 size, AxisSettings settings) {
var xRange = settings.xRange;
var yRange = settings.yRange;
float x = Mathf.Lerp(0, size.x, (value.x - xRange.min) / (xRange.max - xRange.min));
float y = Mathf.Lerp(size.y, 0, (value.y - yRange.min) / (yRange.max - yRange.min));
return new Vector2(x, y);
}
}
util.cs
Util.cs
public static class EnumerableUtil {
/// <summary>
/// ステップサイズで線形に配置されたデータを作成する
/// </summary>
public static IEnumerable<float> LinspaceWithStep(float start, float end, float step) {
if (step <= 0) throw new InvalidOperationException("Step must be positive and non-zero.");
int count = (int)((end - start) / step) + 1;
return Enumerable
.Range(0, count)
.Select(i => start + i * step)
.TakeWhile(value => value <= end);
}
}
【結果】
データの描画
GUI.ClipScope、GUI.GroupScopeのスコープ内では指定したRectのpositionを基準とした相対座標でGUIメソッドを実行できる.また描画される図形も指定Rect内に制限される(Rectからはみ出た部分は描画されない).
デモコード
DemoEditorWindow.cs
private void OnGUI() {
EditorGUILayout.Space();
GUILayout.Label("-------------");
{ // グラフ描画
var settings = new AxisSettings(xRange: (0, 10), yRange: (0, 10), xStep: 1, yStep: 2);
var dummyData = EnumerableUtil.LinspaceWithStep(-2, 12, 0.5f)
.Select(x => new Vector2(x, Mathf.Sin(x) + x))
.ToArray();
var rect = GUILayoutUtility.GetRect(300, 200).Padding(10);
DrawBackDrop(rect, Colors.backdropColor, Colors.gridLineColor);
DrawGrid(rect, settings, Colors.gridLineColor);
DrawData(rect, settings, dummyData, Colors.dataColor);
}
GUILayout.Label("-------------");
}
/// <summary>
/// 点列を描画する
/// </summary>
public void DrawData(Rect rect, AxisSettings settings, Vector2[] values, Color color) {
using (new GUI.ClipScope(rect)) // ※スコープ内のGUIメソッドは指定Rectを基準として処理される
using (new Handles.DrawingScope(color)) {
for (int i = 0; i < values.Length - 1; i++) {
Vector2 p1 = Convertor.ValueToGraphPoint(values[i], rect.size, settings);
Vector2 p2 = Convertor.ValueToGraphPoint(values[i + 1], rect.size, settings);
Handles.DrawLine(p1, p2);
}
foreach (var value in values) {
Vector2 p = Convertor.ValueToGraphPoint(value, rect.size, settings);
Handles.DrawSolidDisc(p, Vector3.forward, 2f);
}
}
}
【結果】
参考
https://tips.hecomi.com/entry/2014/06/23/222805
https://qiita.com/hinagawa/items/4388f2f61cb33b9975e8
デモコード全文
.cs
#if UNITY_EDITOR
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
namespace nitou.EditorScripts {
// デモ表示用のエディタウインドウ
public class DemoEditorWindow : EditorWindow {
[MenuItem(MenuItemName.Prefix.Develop + "Test/Demo Graph")]
public static void Open() => GetWindow<DemoEditorWindow>("Demo Graph");
private void OnGUI() {
EditorGUILayout.Space();
GUILayout.Label("-------------");
{ // グラフ描画
var settings = new AxisSettings(xRange: (0, 10), yRange: (0, 10), xStep: 1, yStep: 2);
var dummyData = EnumerableUtil.LinspaceWithStep(-2, 12, 0.5f)
.Select(x => new Vector2(x, Mathf.Sin(x) + x))
.ToArray();
var rect = GUILayoutUtility.GetRect(300, 200).Padding(10);
DrawBackDrop(rect, Colors.backdropColor, Colors.gridLineColor);
DrawGrid(rect, settings, Colors.gridLineColor);
DrawData(rect, settings, dummyData, Colors.dataColor);
}
GUILayout.Label("-------------");
}
/// <summary>
/// 点列を描画する
/// </summary>
public void DrawData(Rect rect, AxisSettings settings, Vector2[] values, Color color) {
using (new GUI.ClipScope(rect)) // ※スコープ内のGUIメソッドは指定Rectを基準として処理される
using (new Handles.DrawingScope(color)) {
for (int i = 0; i < values.Length - 1; i++) {
Vector2 p1 = Convertor.ValueToGraphPoint(values[i], rect.size, settings);
Vector2 p2 = Convertor.ValueToGraphPoint(values[i + 1], rect.size, settings);
Handles.DrawLine(p1, p2);
}
foreach (var value in values) {
Vector2 p = Convertor.ValueToGraphPoint(value, rect.size, settings);
Handles.DrawSolidDisc(p, Vector3.forward, 2f);
}
}
}
public void DrawGrid(Rect rect, AxisSettings settings, Color gridColor) {
using (new Handles.DrawingScope(gridColor)) {
var graphOrigin = rect.position;
// X軸
var xStart = Mathf.Ceil(settings.xRange.min / settings.xStep) * settings.xStep;
var xTicks = EnumerableUtil.LinspaceWithStep(xStart, settings.xRange.max, settings.xStep);
foreach (var x in xTicks) {
var value1 = new Vector2(x, settings.yRange.min);
var value2 = new Vector2(x, settings.yRange.max);
// GUI座標に変換して描画
var p1 = Convertor.ValueToGraphPoint(value1, rect.size, settings) + graphOrigin;
var p2 = Convertor.ValueToGraphPoint(value2, rect.size, settings) + graphOrigin;
Handles.DrawLine(p1, p2);
}
// Y軸
var yStart = Mathf.Ceil(settings.yRange.min / settings.yStep) * settings.yStep;
var yTicks = EnumerableUtil.LinspaceWithStep(yStart, settings.yRange.max, settings.yStep);
foreach (var y in yTicks) {
var value1 = new Vector2(settings.xRange.min, y);
var value2 = new Vector2(settings.xRange.max, y);
// GUI座標に変換して描画
var p1 = Convertor.ValueToGraphPoint(value1, rect.size, settings) + graphOrigin;
var p2 = Convertor.ValueToGraphPoint(value2, rect.size, settings) + graphOrigin;
Handles.DrawLine(p1, p2);
}
}
}
/// <summary>
/// 背景と枠線を描画する
/// </summary>
public void DrawBackDrop(Rect rect, Color backdropColor, Color outlineColor) {
Handles.DrawSolidRectangleWithOutline(rect, backdropColor, outlineColor);
}
private static class Colors {
public static readonly Color backdropColor = new Color(0.8f, 0.8f, 0.8f, 0.1f);
public static readonly Color gridLineColor = new Color(0.9f, 0.9f, 0.9f, 0.5f);
public static readonly Color dataColor = Color.green;
}
public struct AxisSettings {
// グラフの値域
public (float min, float max) xRange;
public (float min, float max) yRange;
// グリッド幅
public float xStep;
public float yStep;
public AxisSettings((float min, float max) xRange, (float min, float max) yRange, float xStep, float yStep) {
this.xRange = xRange;
this.yRange = yRange;
this.xStep = xStep;
this.yStep = yStep;
}
}
private static class Convertor {
/// <summary>
/// グラフ範囲の座標に変換する
/// </summary>
public static Vector2 ValueToGraphPoint(Vector2 value, Vector2 size, AxisSettings settings) {
var xRange = settings.xRange;
var yRange = settings.yRange;
float x = Mathf.Lerp(0, size.x, (value.x - xRange.min) / (xRange.max - xRange.min));
float y = Mathf.Lerp(size.y, 0, (value.y - yRange.min) / (yRange.max - yRange.min));
return new Vector2(x, y);
}
}
}
}
#endif