はじめに
最近Unityで様々な拡張機能を作っている主です。今回は単位ベクトル専用変数(以後UnitVector)を変数としてもインスペクターからもいじれるようにしてみました。
【今回伝えたい主な内容】
- 実際に拡張機能を作ると考えることが結構多い
- 自分だけではなく、作業者が気軽に使えるようにするにはどうすればいいかを考える必要がある
【もくじ】
- なぜ作ろうと思ったのか
- この変数を作ることよりも、拡張して何かを作るために何を考えればよいかを学んでほしい
- 実装前に理解しておくこと
- 今回実装を行うために必要な単位ベクトルの情報
- 高校で習った内容から応用する必要がある
- 実装例
- 型定義
- インスペクター
- インスペクターの値変更をどうするか。ただ、値が変更できれば良いのか。作業者が使いやすくなるにはどうすればよいか
- 実装例計算説明
- おまけ
- 今回の実装や、その他の用途で使えるような拡張
なぜ作ろうと思ったのか
- 変数から単位ベクトル専用ということが分からない
- Vector(n)で通常管理されることがあるが、その単位ベクトルが別用途で使われる可能性を避けるため
- 誰目線で見ても単位ベクトル目的で使われることをはっきりさせるため
実装前に理解しておくこと
Q. そもそも単位ベクトルとは?(自分は数学が得意だったので理解はしています)
A. 単位ベクトルとは、大きさが 1 であるベクトルのこと
Q. 計算方法は?
A. ベクトルaの単位ベクトルはa/|a|
- 考慮すべき点はそこまで多くない
- と思われるが、実際はそこまで簡単な話ではない
実装例
- 実装設定
- Vector3型(今回は実装しませんがVector2でも作れます)
- インスペクターからも操作可能
型定義
Serializable]
public struct UnitVector3
{
public Vector3 Vector3;
public BoolVector3 BoolVector3; // boolをVector3型で管理できる自作変数
public UnitVector3(Vector3 vector, bool xIsMinusValue, bool yIsMinusValue, bool zIsMinusValue)
{
// Vector2を定義
Vector3 = vector;
BoolVector3 = new BoolVector3(xIsMinusValue, yIsMinusValue, zIsMinusValue);
// 絶対値の合計を計算
var sumOfAbs = Mathf.Abs(Vector3.x) + Mathf.Abs(Vector3.y) + Mathf.Abs(Vector3.z);
/// 合計が0の場合は、初期値として(1, 0, 0)に設定
(処理)
/// それ以外の場合は正規化
}
}
簡易実装になります。こちらで要件は満たしているはずです。
筆者も多くの環境で使えていないため別途機能追加していく予定です。
インスペクターで操作可能に
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(UnitVector3))]
internal class UnitVector3Drawer : PropertyDrawer
{
// refを使用するためにclass型にしているが、変えたほうがいい気もする
// そもそもこれを使うのが最適解かどうかを精査できていないため今後要確認すべきではある
private class Vector3SimpleData
{
public Vector3Extensions.Axis Axis;
public float Value;
public bool IsMinusValue;
public Vector3SimpleData(Vector3Extensions.Axis axis, float value, bool isMinusValue)
{
Axis = axis;
Value = value;
IsMinusValue = isMinusValue;
}
}
private Vector3 _cachedVectorValue;
private BoolVector3 _cachedBoolVector3;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
var vectorProperty = property.FindPropertyRelative("Vector3");
var axisIsMinusValueProperties = property.FindPropertyRelative("BoolVector3");
// それぞれキャッシュの値がセットされていない場合はセットする
if (_cachedVectorValue == Vector3.zero)
{
_cachedVectorValue = vectorProperty.vector3Value;
}
if (_cachedBoolVector3 == new BoolVector3(false, false, false))
{
_cachedBoolVector3 = axisIsMinusValueProperties.BoolVector3Value();
}
EditorGUILayout.BeginVertical();
{
var indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
// フィールドの幅と行の高さを定義
float originalY = position.y;
float lineHeight = EditorGUIUtility.singleLineHeight;
float fieldHeight = lineHeight + EditorGUIUtility.standardVerticalSpacing;
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
var vector3Rect = new Rect(position.x, originalY, position.width, lineHeight);
var boolRect = new Rect(position.x, originalY + fieldHeight, position.width, lineHeight);
ChangeBoolProperty(boolRect, axisIsMinusValueProperties, vectorProperty);
ChangeVectorProperty(vector3Rect, boolRect.y, vectorProperty, axisIsMinusValueProperties);
// 元のインデントに戻す
EditorGUI.indentLevel = indentLevel;
}
EditorGUILayout.EndVertical();
EditorGUI.EndProperty();
}
private void ChangeBoolProperty(Rect rect, SerializedProperty boolVector3property, SerializedProperty vectorProperty)
{
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(rect, boolVector3property, GUIContent.none);
if (EditorGUI.EndChangeCheck())
{
var axises = Enum.GetValues(typeof(Vector3Extensions.Axis));
for (int i = 0; i < axises.Length; i++)
{
/// x, y, zの3要素に対して、もしチェックボックスの値が変わった場合に、その軸のboolVector3の符号を反転する。ただし、元からその値になぜかなっている場合はスキップする(符号が逆になるため)。
(処理)
/// boolVector3の符号が変わったのでvector3の値も反転する
}
}
}
private void ChangeVectorProperty(Rect rect, float attentionSpace, SerializedProperty vectorProperty, SerializedProperty boolVector3Property)
{
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(rect, vectorProperty, GUIContent.none);
if (EditorGUI.EndChangeCheck())
{
var newVector = vectorProperty.vector3Value;
var diffVector = newVector.FindDifferentAxis(_cachedVectorValue);
var isStayedCachedValue = false;
vectorProperty.vector3Value = AdjustValue(newVector, diffVector, boolVector3Property.BoolVector3Value(), ref isStayedCachedValue);
if (!isStayedCachedValue)
{
_cachedVectorValue = vectorProperty.vector3Value;
}
}
EditorGUILayout.Space(attentionSpace);
EditorGUILayout.HelpBox("xとyとzの絶対値の合計が1になるように、比率を維持して調整しています。初期値のままの場合は(x, y, z) = (1, 0, 0)として扱われます", MessageType.Warning);
}
// diffVectorでどの軸の値が変わったのかを把握する
private Vector3 AdjustValue(Vector3 newVector, Vector3 diffVector, BoolVector3 boolVector3, ref bool isStayedCachedValue)
{
var changedValue = new Vector3SimpleData(Vector3Extensions.Axis.X, 0, false);
List<Vector3SimpleData> otherValues = new();
List<Vector3SimpleData> allValues = new();
var axises = Enum.GetValues(typeof(Vector3Extensions.Axis));
var newVectorArray = newVector.Array();
var boolVector3Array = boolVector3.Array();
/// changedValueにインスペクターでいじった値を、otherValuesにそれ以外の値を入れる
(処理)
///
allValues.AddRange(otherValues);
allValues.Add(changedValue);
var changedValueAxisIndex = Array.IndexOf(axises, changedValue.Axis);
/// 以下3パターンで処理を分ける
/// changedValueが別の符号の値になる場合(ちなみに今回はboolVector3の値を変更する方法以外で符号変更は許していません)
/// changedValueの絶対値が1以上
/// それ以外
(処理)
///
List<Vector3SimpleData> allAxisValues = new(otherValues);
allAxisValues.Add(changedValue);
return (allValuesからx, y, zに該当するものをそれぞれ取得してVector3で返す);
}
private void ClampValue(ref float changedValue, ref float otherValue1, ref float otherValue2)
{
changedValue = changedValue > 0 ? 1 : -1;
otherValue1 = 0;
otherValue2 = 0;
}
/// <param name="changedValueCachedValue">変更された値の元の値</param>
/// 残り2つの値に均等に値を分配する際にdiffの値を使用すると、想定した値より大きくなる可能性があるため元の値を使う
/// 例えば、元の値が0.1の場合に0.2の変更が入ると変更後の値は0.0のはずなのに0.2の変更が入ったとみなされ他の2つの値に0.1の変更が加わる可能性があるため
private void ClampValue(ref float otherValue1, ref float otherValue2, ref float changedValue, float changedValueCachedValue)
{
/// CalculateDistributedValueを用いて、otherValue(n)にそれぞれ分配する
(処理)
///
changedValue = 0;
}
// 減った値の分ほかの軸に値を渡す。
// 分配量はそれぞれの軸の大きさに比例する
private void CalculateDistributedValue(ref List<float> allValues, float diffValue)
{
var allValueSum = allValues.Sum(value => Mathf.Abs(value));
for (int i = 0; i < allValues.Count; i++)
{
allValues[i] /= allValueSum;
}
}
// 変更する軸の値を固定したい場合
private void CalculateDistributedValue(ref List<Vector3SimpleData> allSimpleDatas, Vector3 diffVector)
{
}
}
#endif
おおまかな実装です。
今回の肝
- 正規化
- 普段Vector3.normalizedで取得できるがインスペクターから値を設定する場合はどうするか
【どこかしらの軸が絶対値1の場合】
まずは簡単なVector2で考えてみましょう。
ex) `(1, 0)の単位ベクトルに対して, x -= αをする
A. 変化しない
説明
- 「単位ベクトルだから大きさが1になればいい」という考えでy軸の値を変化させたくなる
- そもそも図形で考えたときに違和感がある
- 正規化というところで考えると当たり前
- インスペクターだから、という理由で動かすにはうーんという感じ
【すべての軸が0より大きく、1より小さい値を持っている場合】
次に (α, β, γ)(0 <|α|, |β| < 1, |γ| < 1) の条件で値を増減させたときを考えます。ただ、いきなりこの条件で考えるのは難しいためから(1, 0, 0)に対してz方向に値を加えた場合を考えます。
ex) (1, 0, 0)に対して z += 0.1f をした場合 x, y の値
A. (0.9950..., 0, 0.0995)
説明
- 新しくできたベクトルは(1, 0, 0.1)
- 合計の長さ = 1.01
- x^2 + y^2 + z^2 = 1
- x = 1 / √1.01 ≒ 0.9950
- y = 0 / √1.01 = 0
- z = 0.1 / √1.01 ≒ 0.0995
ということで汎用例です。
ex) (α, β, γ) (0 <= |α|, |β|, |γ| <= 1, √(α^2 + β^2 + γ^2) = 1)
の単位ベクトルに対して x += 0.3f
をした場合
A. ((α+1) / z, β / z, γ / z) (z = √((α + 0.3f)^2 + β^2 + γ^2))
ただ、インスペクターから値を入れるときに固定値を入れたいという場合があると思います。
なのでそういう場合にも対応できるようにします。
こちらはいきなり汎用例です。
ex)(α, β, γ) (0 <= |α|, |β|, |γ| <= 1, √(α^2 + β^2 + γ^2) = 1) の単位ベクトルに対して x = ε (0 <=|ε| <= 1) をした場合
A. (ε, √(1 - ε^2) * (β^2 / (β^2 + γ^2)), y * (γ / β))
説明
- x = ε にしたことでy, zの長さの合計値は √(1 - ε^2)になる
- 次の式が成り立つ
- √(y^2 + z^2) = √(1 - ε^2), y^2 + z^2 = (1 - ε^2)
- 上記の長さをβ, γの比率で分ける
- y : z = β : γ , z = yγ/β
- 先ほどの式に代入
- y^2 + (yγ/β)^2 = (1 - ε^2) , y^2 = (1 - ε^2) * (β^2 / (β^2 + γ^2))
- zはyが求まれば値が出るため、上記を求めれば求まる
おまけ
単位ベクトルにn倍してVector3型として返す
public static Vector3 operator *(UnitVector3 v, float f)
{
return v.Vector3 * f;
}
public static Vector3 operator *(float f, UnitVector3 v)
{
return v.Vector3 * f;
}
Quaternionに掛けてVector3型として返す
public static Vector3 operator *(UnitVector3 v, Quaternion q)
{
return q * v.Vector3;
}
public static Vector3 operator *(Quaternion q, UnitVector3 v)
{
return q * v.Vector3;
}
Vector3のそれぞれの軸をArrayとして返す
public static float[] Array(this Vector3 vector) => new[] { vector.x, vector.y, vector.z };
おわりに
今回は単位ベクトル専用変数を作成してインスペクターからも値がいじれるようにしました。
具体的にEditor拡張をするにはどうすればいいんだ~という疑問を持たれている方もいらっしゃるとは思いますので別の記事に時間があるときに記載していこうと思います。