はじめに
UnityのTransformとは
親子関係を持ち、親に対するローカルの座標、角度、拡大率などからグローバルの値に変換するものです。(雑)
詳しくは、はしょります。
親子関係を持てるのが結構使いやすいです。
今回作るもの
ということで、それっぽいものをC++で作ってみることにしますが、面倒くさいので次元を1つさげて2D用で実装してみることとします。
考え方は同じなので3D用も容易に実装できると思います。
2Dの座標変換に必要なもの
- 座標(Vector2)
- Z軸回転角度(double)
- 拡大率(Vector2)
今回使用する環境
言語はC++で
Vec2やMat3x2といったclassが提供されているのでSiv3Dというライブラリを使用しますが、簡単に説明します。
Vec2について
以下のような2Dベクトルの構造体だと思ってください
struct Vec2
{
double x,y;
};
Mat3x2について
2Dの場合同次座標の3列目が固定なため、3x2行列のデータで、アフィン変換が可能です。
少しおさらいしておきましょう。
移動
\begin{bmatrix}
x^{,}\\\
y^{,}\\\
1
\end{bmatrix}
=
\begin{bmatrix}
1 & 0 & a\\\
0 & 1 & b\\\
0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
x\\\
y\\\
1
\end{bmatrix}
回転
\begin{bmatrix}
x^{,}\\\
y^{,}\\\
1
\end{bmatrix}
=
\begin{bmatrix}
cos(r) & -sin(r) & 0\\\
sin(r) & cos(r) & 0\\\
0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
x\\\
y\\\
1
\end{bmatrix}
拡大
\begin{bmatrix}
x^{,}\\\
y^{,}\\\
1
\end{bmatrix}
=
\begin{bmatrix}
s_{x} & 0 & 0\\\
0 & s_{y} & 0\\\
0 & 0 & 1
\end{bmatrix}
\begin{bmatrix}
x\\\
y\\\
1
\end{bmatrix}
コード
全体像
方針
変換に必要な情報を持つ構造体をもったローカルとグローバルの変数をもつTransform classを作り、メンバに親と子供のポインタを持ちます。
自身の座標が変更されたとき、全ての子供にそれを通知し、子供の新しい座標を改めて計算します。
ざっくりと
class Transform
{
private:
//ローカル座標とグローバル座標
struct Elm
{
Vec2 m_pos;
double m_rotate;
Vec2 m_scale;
Elm() :
m_pos(0, 0),
m_rotate(0),
m_scale(1, 1)
{}
Elm(const Vec2& pos, const double rotate, const Vec2& scale) :
m_pos(pos),
m_rotate(rotate),
m_scale(scale)
{}
}m_local, m_global;
//親
Transform *m_parent = nullptr;
//子供
std::list<Transform*> m_childs;
//子供の追加
bool addChild(Transform* const child);
//子供の削除
void deleteChild(Transform* const child);
public:
Transform() = default;
Transform(const Vec2 & pos, const double rotate, const Vec2 & scale = { 1,1 }, Transform * const parent = nullptr);
~Transform();
};
あとは、これに通知の関数やセッター等を追加すればできそうです。
親のパラメータから自身のパラメータを求める
上のclassに
通知関数
void Transform::notifyUpdate()
関数を追加します。
この関数は、自身のローカル座標が直接変更されたとき、親の座標が変更されたときに呼ばれます。
実装
void Transform::notifyUpdate()
{
//ローカル、親の変更から、実際のグローバル座標を調整
if (m_parent != nullptr)
{
//座標
const auto m =
Mat3x2::Scale(m_parent->m_global.m_scale).
rotate(m_parent->m_global.m_rotate);
m_global.m_pos = m_parent->m_global.m_pos + m.transform(m_local.m_pos);
//回転角度
m_global.m_rotate = m_local.m_rotate + m_parent->m_global.m_rotate;
//拡大率
m_global.m_scale.x = m_local.m_scale.x*m_parent->m_global.m_scale.x;
m_global.m_scale.y = m_local.m_scale.y*m_parent->m_global.m_scale.y;
}
else
{
m_global.m_pos = m_local.m_pos;
m_global.m_rotate = m_local.m_rotate;
m_global.m_scale = m_local.m_scale;
}
//変更を子供に通知
for (auto&& child : m_childs)
{
if (child != nullptr)
child->notifyUpdate();
}
}
実際の位置は
A=親の拡大率による拡大行列*親の角度による回転行列
とすると
親の座標+A*自身のローカル座標
実際の回転角度は
親の回転角度+自身のローカル回転角度
実際の拡大率は
親の拡大率*自身のローカル拡大率
でそれぞれ求められます。
もし親がいない場合は自身のローカルパラメータとグローバルパラメータは全く同じものになります。
ローカルの値を変更したら
ローカルの座標を変更する以下のような関数を作ったら、値を書き換えたあとに通知関数を呼びます。
void Transform::_setLocalPos(const Vec2 & pos)
{
m_local.m_pos = pos;
this->notifyUpdate();
}
通知関数をpublicにしてclass使用者がすべての値変更後最後に必ず呼ぶように管理すれば実行速度はあげられますが使いやすさを求めて今回は中で呼ぶようにします。
同様に、ローカル拡大率や、ローカル回転角度の変更関数も作れます。
グローバルパラメータを変更したときにローカルパラメータを求める
値の変更はローカルだけでなくグローバルの値を直接変更することも考えてみます。
グローバルが変わったことによりローカルもかわるはずなので再計算がひつようになるはずです。
グローバルの値が変更された時の通知関数
void Transform::notifyGlobalUpdate()
{
//グローバル座標を変換したときにローカル座標も調整
if (m_parent != nullptr)
{
const auto m =
Mat3x2::Rotate(-m_parent->m_global.m_rotate).scale({ 1 / m_parent->m_global.m_scale.x, 1 / m_parent->m_global.m_scale.y });
m_local.m_pos = m.transform(m_global.m_pos - m_parent->m_global.m_pos);
m_local.m_rotate = m_global.m_rotate - m_parent->m_global.m_rotate;
m_local.m_scale.x = m_global.m_scale.x / m_parent->m_global.m_scale.x;
m_local.m_scale.y = m_global.m_scale.y / m_parent->m_global.m_scale.y;
}
else
{
m_local.m_pos = m_global.m_pos;
m_local.m_rotate = m_global.m_rotate;
m_local.m_scale = m_global.m_scale;
}
//変更を子供に通知
for (auto&& child : m_childs)
{
if (child != nullptr)
child->notifyUpdate();
}
}
グローバルからローカルを求めるには、さきほどローカルからグローバルにするためにした操作と全く真逆のことをする必要があります。
ローカルの位置は
Aの逆行列*(グローバル座標-親の座標)
ローカルの回転角度は
グローバル回転角度-親の回転角度
ローカルの拡大率は
グローバル拡大率/親の拡大率
でそれぞれ求められます。
グローバルの値を変更したら
さきほど同様に値の変更+通知関数を使用します。
void Transform::_setPos(const Vec2 & pos)
{
m_global.m_pos = pos;
this->notifyGlobalUpdate();
}
子供の追加、削除の関数
//追加
bool Transform::addChild(Transform * const child)
{
if (child == nullptr)
return false;
//もし自分が祖先なら追加失敗
Transform* check = this;
do
{
if (check == child)
{
return false;
}
check = check->m_parent;
} while (check != nullptr);
//今の親から自分が子供であることは忘れてもらう
auto prevParent = child->m_parent;
if (prevParent != nullptr)
prevParent->deleteChild(child);
//子供の親になる
child->m_parent = this;
m_childs.emplace_back(child);
return true;
}
//削除
void Transform::deleteChild(Transform * const child)
{
if (child != nullptr && !m_childs.empty())
{
Erase(m_childs, child);
}
}
削除は簡単ですが、追加について解説
もし自分の祖先に自分の子供がいたらおかしくなってしまいます。
したがって子供を追加するときにもし自分がすでに子供の祖先に存在してるときには失敗するようにしています。
失敗しなかった場合は、子供のポインタをリストに追加して自身を子供の親にします。
その際、もともと別の親が設定されていたならば自分を子供から消してもらいます。
親の指定
bool Transform::_setParent(Transform * const parent)
{
if (parent != nullptr)
{
if (!parent->addChild(this))
return false;
}
else
{
if (m_parent != nullptr)
m_parent->deleteChild(this);
m_parent = parent;
}
_setPos(this->m_global.m_pos);
_setRotate(this->m_global.m_rotate);
_setScale(this->m_global.m_scale);
return true;
}
親を指定するときに、親の子供追加関数に自身をわたします。
もし親をnullptrにしたいときは、もともと親がいたなら自身を子供から削除してもらいます
このとき、親が指定されたことによってローカルパラメータが変わる可能性があるので各セッターを呼んでいます。
あと始末
最後にデストラクタを忘れず書きます。
Transform::~Transform()
{
if (m_parent != nullptr)
{
m_parent->deleteChild(this);
}
for (auto&& elm : m_childs)
{
if (elm != nullptr)
{
elm->m_parent = nullptr;
}
}
}
自身が破棄されたときに、親がいたなら自分をリストから削除してもらい
子供がいたなら子供の親をnullptrにします。
できた
これでUnityのように親子関係をもったTransform classを作ることができました。
まとめ
- ローカルとグローバルの関係を常に保つために、片方が変わったときはもう片方も変更する必要がある
- 値を変えるたびに通知が起きるので速度低下が課題
- 使いやすくて、もっといい実装ないのか?
おまけ
プロパティなどを利用して
もっと使いやすくしたclass
完成版
class PropertyVec2;
class Transform
{
friend PropertyVec2;
private:
//ローカル座標とグローバル座標
struct Elm
{
Vec2 m_pos;
double m_rotate;
Vec2 m_scale;
Elm() :
m_pos(0, 0),
m_rotate(0),
m_scale(1, 1)
{}
Elm(const Vec2& pos, const double rotate, const Vec2& scale) :
m_pos(pos),
m_rotate(rotate),
m_scale(scale)
{}
}m_local, m_global;
//親
Transform *m_parent = nullptr;
//子供
std::list<Transform*> m_childs;
//更新関数
void notifyGlobalUpdate();
void notifyUpdate();
//子供の追加
bool addChild(Transform* const child);
//子供の削除
void deleteChild(Transform* const child);
public:
Transform() = default;
Transform(const Vec2 & pos, const double rotate, const Vec2 & scale = { 1,1 }, Transform * const parent = nullptr);
Transform(const Vec2 & pos, const double rotate, const double & scale, Transform * const parent = nullptr);
Transform(const Vec2 & pos);
Transform(const Vec2 & pos, Transform * const parent);
~Transform();
//代入
Transform& operator =(const Transform& other);
//プロパティ
//グローバル
_declspec(property(get = _getPos, put = _setPos))Vec2& pos;
_declspec(property(get = _getPos, put = _setPos))PropertyVec2 pos;
_declspec(property(get = _getRotate, put = _setRotate))double rotate;
_declspec(property(get = _getScale, put = _setScale))Vec2& scale;
_declspec(property(get = _getScale, put = _setScale))PropertyVec2 scale;
//ローカル
_declspec(property(get = _getLocalPos, put = _setLocalPos))Vec2& localPos;
_declspec(property(get = _getLocalPos, put = _setLocalPos))PropertyVec2 localPos;
_declspec(property(get = _getLocalRotate, put = _setLocalRotate))double localRotate;
_declspec(property(get = _getLocalScale, put = _setLocalScale))Vec2& localScale;
_declspec(property(get = _getLocalScale, put = _setLocalScale))PropertyVec2 localScale;
//親
_declspec(property(get = _getParent, put = _setParent))Transform* parent;
bool _setParent(Transform*const parent);
const Transform*const _getParent()const;
void _setPos(const Vec2& pos);
PropertyVec2 _getPos();
const Vec2& _getPos()const;
void _setRotate(const double angle);
const double _getRotate()const;
void _setScale(const Vec2& scale);
void _setScale(const double scale);
PropertyVec2 _getScale();
const Vec2& _getScale()const;
void _setLocalPos(const Vec2& pos);
PropertyVec2 _getLocalPos();
const Vec2& _getLocalPos()const;
void _setLocalRotate(const double angle);
const double _getLocalRotate()const;
void _setLocalScale(const Vec2& scale);
void _setLocalScale(const double scale);
PropertyVec2 _getLocalScale();
const Vec2& _getLocalScale()const;
};
//プロパティ用Vec2
//Vec2型のメンバ変数の変更後にトランスフォームに通知する
class PropertyVec2 :public Vec2
{
friend class Transform;
private:
Transform*const m_transform;
Vec2*const m_value;
bool m_isLocal;
PropertyVec2(Transform*const me, Vec2*const value, bool local) :
Vec2(value->x, value->y),
m_transform(me),
m_value(value),
m_isLocal(local)
{}
public:
__declspec(property(get = _getX, put = _setX))double x;
__declspec(property(get = _getY, put = _setY))double y;
double _getX()const
{
return m_value->x;
}
void _setX(double x)
{
m_value->x = x;
if (!m_isLocal)
m_transform->notifyGlobalUpdate();
else
m_transform->notifyUpdate();
}
double _getY()const
{
return m_value->y;
}
void _setY(double y)
{
m_value->y = y;
if (!m_isLocal)
m_transform->notifyGlobalUpdate();
else
m_transform->notifyUpdate();
}
};
Transform::Transform(const Vec2& pos, const double rotate, const Vec2& scale, Transform* const parent) :
m_global(pos, rotate, scale),
m_local(pos, rotate, scale),
m_parent(nullptr)
{
this->parent = parent;
}
Transform::Transform(const Vec2& pos, const double rotate, const double& scale, Transform* const parent) :
Transform(pos, rotate, { scale,scale }, parent)
{}
Transform::Transform(const Vec2& pos) :
Transform(pos, 0, { 1,1 }, nullptr)
{}
Transform::Transform(const Vec2& pos, Transform* const parent) :
Transform(pos, 0, { 1,1 }, parent)
{}
void Transform::notifyGlobalUpdate()
{
//グローバル座標を変換したときにローカル座標も調整
if (m_parent != nullptr)
{
const auto m =
Mat3x2::Rotate(-m_parent->m_global.m_rotate).scale({ 1 / m_parent->m_global.m_scale.x, 1 / m_parent->m_global.m_scale.y });
m_local.m_pos = m.transform(m_global.m_pos - m_parent->m_global.m_pos);
m_local.m_rotate = m_global.m_rotate - m_parent->m_global.m_rotate;
m_local.m_scale.x = m_global.m_scale.x / m_parent->m_global.m_scale.x;
m_local.m_scale.y = m_global.m_scale.y / m_parent->m_global.m_scale.y;
}
else
{
m_local.m_pos = m_global.m_pos;
m_local.m_rotate = m_global.m_rotate;
m_local.m_scale = m_global.m_scale;
}
//変更を子供に通知
for (auto&& child : m_childs)
{
if (child != nullptr)
child->notifyUpdate();
}
}
void Transform::notifyUpdate()
{
//ローカル、親の変更から、実際のグローバル座標を調整
if (m_parent != nullptr)
{
const auto m =
Mat3x2::Scale(m_parent->m_global.m_scale).
rotate(m_parent->m_global.m_rotate);
m_global.m_pos = m_parent->m_global.m_pos + m.transform(m_local.m_pos);
m_global.m_rotate = m_local.m_rotate + m_parent->m_global.m_rotate;
m_global.m_scale.x = m_local.m_scale.x*m_parent->m_global.m_scale.x;
m_global.m_scale.y = m_local.m_scale.y*m_parent->m_global.m_scale.y;
}
else
{
m_global.m_pos = m_local.m_pos;
m_global.m_rotate = m_local.m_rotate;
m_global.m_scale = m_local.m_scale;
}
//変更を子供に通知
for (auto&& child : m_childs)
{
if (child != nullptr)
child->notifyUpdate();
}
}
bool Transform::addChild(Transform * const child)
{
if (child == nullptr)
return false;
//もし自分が祖先なら追加失敗
Transform* check = this;
do
{
if (check == child)
{
return false;
}
check = check->m_parent;
} while (check != nullptr);
//今の親から自分が子供であることは忘れてもらう
auto prevParent = child->m_parent;
if (prevParent != nullptr)
prevParent->deleteChild(child);
//子供の親になる
child->m_parent = this;
m_childs.emplace_back(child);
return true;
}
void Transform::deleteChild(Transform * const child)
{
if (child != nullptr && !m_childs.empty())
{
Erase(m_childs, child);
}
}
Transform::~Transform()
{
if (m_parent != nullptr)
{
m_parent->deleteChild(this);
}
for (auto&& elm : m_childs)
{
if (elm != nullptr)
{
elm->m_parent = nullptr;
}
}
}
bool Transform::_setParent(Transform * const parent)
{
if (parent != nullptr)
{
if (!parent->addChild(this))
return false;
}
else
{
if (m_parent != nullptr)
m_parent->deleteChild(this);
m_parent = parent;
}
_setPos(this->m_global.m_pos);
_setRotate(this->m_global.m_rotate);
_setScale(this->m_global.m_scale);
return true;
}
const Transform * const Transform::_getParent() const
{
return m_parent;
}
const Vec2 & Transform::_getPos() const
{
return m_global.m_pos;
}
PropertyVec2 Transform::_getPos()
{
return{ this,&m_global.m_pos,false };
}
const double Transform::_getRotate() const
{
return m_global.m_rotate;
}
const Vec2 & Transform::_getScale() const
{
return m_global.m_scale;
}
PropertyVec2 Transform::_getScale()
{
return{ this,&m_global.m_scale,false };
}
const Vec2 & Transform::_getLocalPos() const
{
return m_local.m_pos;
}
PropertyVec2 Transform::_getLocalPos()
{
return{ this,&m_local.m_pos,true };
}
const double Transform::_getLocalRotate() const
{
return m_local.m_rotate;
}
const Vec2 & Transform::_getLocalScale() const
{
return m_local.m_scale;
}
PropertyVec2 Transform::_getLocalScale()
{
return{ this,&m_local.m_scale,true };
}
Transform & Transform::operator=(const Transform & other)
{
//セッターのほうを呼ばないと前の親の子供として残ってしまう
this->parent = other.m_parent;
this->m_global = other.m_global;
this->m_local = other.m_local;
//子供についての情報は引き継がない
return *this;
}
void Transform::_setLocalPos(const Vec2 & pos)
{
m_local.m_pos = pos;
this->notifyUpdate();
}
void Transform::_setLocalRotate(const double angle)
{
m_local.m_rotate = angle;
this->notifyUpdate();
}
void Transform::_setLocalScale(const Vec2 & scale)
{
m_local.m_scale = scale;
this->notifyUpdate();
}
void Transform::_setLocalScale(const double scale)
{
_setLocalScale({ scale,scale });
}
void Transform::_setPos(const Vec2 & pos)
{
m_global.m_pos = pos;
this->notifyGlobalUpdate();
}
void Transform::_setRotate(const double angle)
{
m_global.m_rotate = angle;
this->notifyGlobalUpdate();
}
void Transform::_setScale(const Vec2 & scale)
{
m_global.m_scale = scale;
this->notifyGlobalUpdate();
}
void Transform::_setScale(const double scale)
{
_setScale({ scale,scale });
}