🔎

【Unity】親GameObjectの影響を排除したscale算出方法

2024/05/28に公開

階層構造になっているGameObjectで、親のscaleを変更したときに子のサイズは変わってほしくないことがありました。
こういう場合のlocalScaleの算出方法について、検討した案を残しておきます。

案1: まずは正攻法

親の影響を打ち消すスケール算出(正攻法版)
var localScale = this.transform.localScale;
var parentLossyScale = this.transform.parent.lossyScale;

this.transform.localScale
    = new Vector3(
        localScale.x / parentLossyScale.x,
        localScale.y / parentLossyScale.y,
        localScale.z / parentLossyScale.z);

親のlossyScale * 子のlocalScale = 子のlossyScale

となりますが、親のlossyScaleを打ち消したいので、「子のlocalScale / 親のlossyScale」を子のlocalScaleに設定します。

理解しやすいし、これで充分だと思います。
…でも長いしなんだか冗長でもやもやする。
x, y, zを間違えそうでちょっと怖いのもある。
もう少しすっきり書ける方法は無いのだろうか…?

案2: インデクサ利用

Vector3にはインデクサが用意されているので、これを使ってみます。

親の影響を打ち消すスケール算出(インデクサ利用版)
var localScale = this.transform.localScale;
var parentLossyScale = this.transform.parent.lossyScale;

var scaleUnaffectedByParents = Vector3.zero;
Enumerable.Range(0, 3).ToList().ForEach(i =>
    scaleUnaffectedByParents[i] = localScale[i] / parentLossyScale[i]);
this.transform.localScale = scaleUnaffectedByParents;

x, y, zを間違える可能性が無いのが良いですね。
割り算の記述も一箇所になってそれなりにすっきりはしましたが…以下の部分があと一歩足りないように感じてしまいます。

  • マジックナンバーが登場する。
  • Vector3のプロパティにインデクサでアクセスするというのがいまいちしっくり来ない。

案3: Transformのメソッド活用

こんなやり方で算出することも可能です。

親の影響を打ち消すスケール算出(Transformのメソッド活用版)
// localScaleを表すベクトルをワールドの向きに変換
var worldDirectedLocalScale
    = this.transform.parent.TransformDirection(this.transform.localScale);

// ワールド上で上記ベクトルを示す場合のローカル上での表現方法を算出
// これがlocalScale設定値の大きさをワールド上で実現するために必要なlocalScaleとなる
var scaleUnaffectedByParents
    = this.transform.parent.InverseTransformVector(worldDirectedLocalScale);
this.transform.localScale = scaleUnaffectedByParents;

無駄に複雑になってしまいましたが、これってUnityエキスパートの方ならすぐに理解できるものなのでしょうか?

解説

絵を描いてみました。
カプセルの中に入ったUnityちゃんはscale0.75倍。そしてカプセルがscale2倍だったとします。
そしてカプセルは30度回転し、Unityちゃんは45度回転しています。
(表現しやすいので2Dで考えます)

親の影響を排除したscaleの算出方法.png

親のscaleの影響を排除したいということは、「local scale = world scale」となってほしいのです。
つまり、今のlocalScaleの値がworldScaleとなるような値を、localScaleに設定できれば良いのです。

設定したいlocalScale → ワールドへ変換 → 現在のlocalScale
となってほしい。ということは、
現在のlocalScale → ワールドからローカルへ変換 → 設定したいlocalScale

しかしこれだけだと、ローカルスペースとワールドスペースの軸の向きが異なる場合に上手くいきません。
ということで、向きを合わせる必要があります。

結果的に以下の手順で算出することになります。番号を画像内の数字と一致させています。

  1. UnityちゃんのlocalScaleを取得すると(x, y) = (0.75, 0.75)が取得できる。
  2. これをワールドスペース上での向きに変換する。
    30度左に回転しているカプセル空間での(x, y) = (0.75, 0.75)のベクトルは、ワールドスペース上では(x, y) = (約0.27, 約1.02)となる。
  3. このベクトルがワールドスペースにあるときにカプセル空間ではどう表記するのか?がlocalScalに設定すべき値となるので、ベクトルをカプセル空間に変換する。

これでほしい値は算出できるのですが…より理解しやすい書き方があるのにこれだけ解説が必要となる案は悪手だよね、ということで没になりました。
ベクトルを回転させてまた戻すという処理も無駄に入るので、パフォーマンスも下がります。
ベクトルや、ワールドスペースとローカルスペースについての理解を深めるためのクイズとしては機能するかもしれません。

案4: 行列利用

案3がこれだけ複雑になったのは、Transformクラスに用意されている基底変換メソッドが以下の3種類のみだからです。

メソッド名 変換内容
TransformDirecttion() Rotationのみ考慮
TransformVector() Rotation, Scaleを考慮
TransformPoint() Rotation, Scale, Position全てを考慮

今回はScaleを示すベクトルの変換なので、Rotationの考慮は不要です。
しかしScaleのみを考慮して基底変換してくれるパターンはないようです。

便利メソッドがないなら、自分で行列を作れば良いのでは?
ということで作ってみました。

親の影響を打ち消すスケール算出(行列利用版)
// 現在のLocalScaleが最終的に表現したいWorldScale
var aimingWorldScale = this.transform.localScale;

// 親のスケール設定を示す4x4行列
var parentScaleMatrix
    = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, this.transform.parent.lossyScale);

// 目的とするWorldScaleを示すベクトルを、ローカルスペースに変換
// (ただしScaleのみ考慮)
var scaleUnaffectedByParents = parentScaleMatrix.inverse * aimingWorldScale;
this.transform.localScale = scaleUnaffectedByParents;

行列を使うと複雑になり過ぎるかと思って最初は選択肢に入れていませんでしたが、Matrix4x4クラスにも便利メソッドがあったのですっきりと書くことができました。
上記では説明用に処理を分けましたが、まとめたら2行位にできそうです。

案1、2の書き方に抵抗を感じる方はこの案が良いのではないでしょうか。

案5: 案1の応用

ローカル関数を用いると、案1の「xyzを書き間違えるリスクがある」というリスクを下げることができます。

親の影響を打ち消すスケール算出(正攻法版にローカル関数を用いたバージョン)
float CalcScale(Func<Vector3, float> gettingValueFunc) =>
    gettingValueFunc(this.transform.localScale) / gettingValueFunc(this.tarnsform.parent.lossyScale);

this.transform.localScale
    = new Vector3(
        CalcScale(scale => scale.x),
        CalcScale(scale => scale.y),
        CalcScale(scale => scale.z));

ただローカル関数自体が処理内容に対して無駄に複雑になっているような気もするので、案1よりこちらの方が絶対良いと言い切る程にはなれてないように思います。

おまけ: スケールを1倍にする方法

子のlocalScaleの設定に関わらず、親の影響を打ち消して1倍になるように変更したいという場合は、以下の記述となります。[1]

親の影響を打ち消してスケールを1倍に変更
var lossyScale = this.transform.lossyScale;

this.transform.localScale
    = new Vector3(
        1.0f / lossyScale.x,
        1.0f / lossyScale.y,
        1.0f / lossyScale.z);

親がNon-Uniform Scalingの場合は要注意

親GameObjectがNon-Uniform Scaling(不均等スケール)というX, Y, Z軸のスケール値が同じでない場合に、その子GameObjectが回転していると、完全に親の影響を排除することが難しくなります。

詳しくはこちらにまとめました。

https://zenn.dev/lilytechlab/articles/93af5edc750417

脚注
  1. こちらは案1ベースのものですが、案2、5ベースでも記載可能です。 ↩︎

リリテックラボ

Discussion