質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.78%

ローカル座標軸を使ったオブジェクトの動きの抑制

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 1,087

score 15

前提・実現したいこと

ConfigurableJoint連結したカプセルをAddRelativeTorqueを使って曲げています。
曲げる際、オレンジカプセルが曲がった状態のままピタッと止めるようにし、重力を働かせたら曲げ状態を維持したまま落ちるようにしたいと考えてます。
イメージ説明

イメージ説明

イメージ説明

発生している問題・エラーメッセージ

Torqueを使いながら曲げると慣性が働いてしまい、ゲームコントローラーの入力がないときにピタッと止めることができませんでした。
またRigidbodyの『Constraints』のFreezeを使うと今度は、ワールド座標で位置が固定されてしまい(空中に浮いた状態)、重力を働かせることができませんでした。

ほかに試したこと

1.慣性をなくす方法で、以下のURL記載の方法も試しました。
https://teratail.com/questions/105824
↓の速度をゼロにしてしまうと、本来効かせたい重力分までゼロになってしまいます。

rigid.velocity = Vector3.zero;


2.RigidBodyのローカルにConstraintsを働かせる
こちらも試してみました。
https://answers.unity.com/questions/404420/rigidbody-constraints-in-local-space.html
真似して以下のように記載しましたが、ピタッと止めることはできませんでした。

Vector3 LocalSpeed = transform.InverseTransformDirection(rigid.velocity);
LocalSpeed.x = 0;
LocalSpeed.y = 0;
LocalSpeed.z = 0;


ほかにも『GetRelativePoitVelocity』や『magnitude』を使って、曲げスピードに制限をかけるなども試してみましたが、結果は変わらず、ピタッと止めることができませんでした。

やりたいこと

オレンジカプセルを青色カプセルの子に配置し、
『ゲームコントローラの入力がないとき』
⇒『オレンジカプセルのローカル座標だけに、RigidBodyのConstraintsのFreezePositonを働かせる』
ができれば上記の問題が解決できるのではないかと考えてます。

初歩的な質問で申し訳ございませんが、どなたかご教示いただけると幸いです。
以上です。

追記(2020年11月12日)ローカル座標を使ったストップ

回転軸をわかりやすくするため、Sphereで説明します。黄色ボールがスクリプトを当ててトルクで回転させたい部品です。
イメージ説明

ヒエラルキーの階層は、↓のように段階的に親子関係を作ってます。根本となる青ボールが『Sphere』です。
イメージ説明

上記の階層構成を取ることで、個々の黄色ボールのTransformのZ角(25度)の値をきれいに取得することができます。
イメージ説明
↓曲げたときの画像
イメージ説明
親子関係を取らないと、根本(青ボール側)の角度の合算値が出てしまいます。角度指定が難しくなるかと思います。

この階層構成を使い、黄色ボールのローカルZ角度座標を使って、うまく慣性ストップ・位置固定できないかと考えております。例えば、狙いの角度30度曲がれ!と設定して、30度に近づいたら徐々にスピードが弱まり、30度位置に来たら、ローカル座標Z30度だけを位置キープ(フリーズ)する ということができないか考えております。

以上です。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

+3

オレンジカプセルの角度を動かしたくない時だけFixedJointを増設して、両者の位置関係をがっちり拘束してしまう...というのはどうでしょうかね?

using UnityEngine;

[RequireComponent(typeof(Rigidbody), typeof(ConfigurableJoint))]
public class CapsuleController : MonoBehaviour
{
    [SerializeField] private float torqueMagnitude = 5.0f;
    private Rigidbody thisRigidbody;
    private Rigidbody parentRigidbody;
    private FixedJoint fixedJoint;

    private float input;

    private void Start()
    {
        this.thisRigidbody = this.GetComponent<Rigidbody>(); // オレンジ色のカプセル
        this.parentRigidbody = this.transform.parent.GetComponent<Rigidbody>(); // 青色のカプセル
        this.SetKinematic(true);
    }

    private void SetKinematic(bool value)
    {
        if (value)
        {
            this.parentRigidbody.isKinematic = true;
            this.thisRigidbody.useGravity = false;
            this.thisRigidbody.velocity = Vector3.zero;
            this.thisRigidbody.angularVelocity = Vector3.zero;
            this.Lock();
        }
        else
        {
            this.parentRigidbody.isKinematic = false;
            this.thisRigidbody.useGravity = true;
            this.Lock();
        }
    }

    private void Lock()
    {
        if (this.fixedJoint != null)
        {
            return;
        }

        this.fixedJoint = this.gameObject.AddComponent<FixedJoint>();
        this.fixedJoint.connectedBody = this.parentRigidbody;
    }

    private void Free()
    {
        if (this.fixedJoint == null)
        {
            return;
        }

        Destroy(this.fixedJoint);
        this.fixedJoint = null;
    }

    private void Update()
    {
        this.input = this.parentRigidbody.isKinematic ? Input.GetAxis("Vertical") : 0.0f;

        // 入力がなければFixedJointを取り付けてロック、入力があれば解除
        if (Mathf.Approximately(this.input, 0.0f))
        {
            this.Lock();
        }
        else
        {
            this.Free();
        }

        // スペースバーで青カプセルのisKinematicをオフ、
        // オレンジカプセルのuseGravityをオンにして落下開始
        if (Input.GetKeyDown(KeyCode.Space))
        {
            this.SetKinematic(false);
        }
    }

    private void FixedUpdate()
    {
        // 今回の実験では、オレンジカプセルの回転軸がローカルX軸になるようにしてしまったため
        // X軸周りのトルクをかけていますが、ご質問者さんの場合はY軸ということでしょうかね?
        this.thisRigidbody.AddRelativeTorque(this.input * this.torqueMagnitude, 0.0f, 0.0f);
    }
}

図

追記

根拠のない直感なんですが、ヘビ型ロボットのようなものを作る布石ということを念頭に置くと、個々の体節が駆動する仕組みはなるべくシンプルにしておいた方がいいような予感がします。
駆動方法についても、個人的には当初のAddRelativeTorqueによる方法は「オレンジカプセルを指でつまんでひねる」といったイメージ...つまりヘビ本体の動力ではなく、外部からの力で回転しているような感じがしました。それよりもジョイント部分に仕込まれたモーターで回転させるイメージの方がしっくりくる気がするのですが、いかがでしょうかね?

オレンジカプセルのスクリプトを下記のようにしてみました。本質的なのはUpdateFixedUpdateで、Start内ではジョイントのパラメーターを設定しているだけです。
ここで設定しなくてもジョイントのインスペクター上に直接入力すればいいのですが、やたら設定項目が多くてどれをいじったか忘れてしまいそうになりこのようにしました。

using UnityEngine;

[RequireComponent(typeof(ConfigurableJoint))]
public class CapsuleController2 : MonoBehaviour
{
    [SerializeField] private float angularSpeedInDegrees = 120.0f;
    [SerializeField] private float damper = 100.0f;
    [SerializeField] private float angularLimitInDegrees = 60.0f;

    private ConfigurableJoint configurableJoint;
    private float input;

    private void Start()
    {
        this.configurableJoint = this.GetComponent<ConfigurableJoint>();
        this.configurableJoint.xMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.yMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.zMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.angularXMotion = ConfigurableJointMotion.Limited;
        this.configurableJoint.angularYMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.angularZMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.highAngularXLimit = new SoftJointLimit
        {
            limit = this.angularLimitInDegrees
        };
        this.configurableJoint.lowAngularXLimit = new SoftJointLimit
        {
            limit = -this.angularLimitInDegrees
        };
        this.configurableJoint.angularXDrive = new JointDrive
        {
            maximumForce = float.MaxValue,
            positionSpring = 0.0f,
            positionDamper = this.damper
        };
    }

    private void Update()
    {
        this.input = Input.GetAxis("Vertical");
    }

    private void FixedUpdate()
    {
        this.configurableJoint.targetAngularVelocity = new Vector3(
            this.input * this.angularSpeedInDegrees * Mathf.Deg2Rad,
            0.0f,
            0.0f);
    }
}

また、両カプセルのコライダーはBoxColliderに差し替えました。丸いカプセルではなく角柱にすることで、地面の上に落ちた後ふとしたはずみにコケてしまうのを防ごうという魂胆です。Freeze Rotation Yをオンにすることも考えたのですが、力の解決の結果すごく大きな力がかかることがあるようで、たまに空高く吹っ飛んでしまうことがありました。

図1

他にも、青カプセルとオレンジカプセルを親子関係にするのはやめました。親子関係になっていると、青カプセルが移動・回転した時に子オブジェクトであるオレンジカプセルも移動・回転してしまうことになります。経験上、物理シミュレーションの制御下にあるオブジェクトの位置や姿勢をダイレクトに操作するとろくなことにならないと思っておりまして、このようなオレンジカプセルの移動・回転もそれと同様の性質を持つだろうと考えてのことです。

動かしてみると下図のようになりました。コメント欄で申し上げたたとえ話の、スカイダイビングをする人のような挙動になっています。

図2

また、このようなコンセプトで駆動する体節を鎖状に繋げるとどんな挙動をするだろうかと気になりまして、下図のようにオブジェクトをセットアップしてみました。Headが青カプセル、Segment1~9がオレンジカプセルで、Segment1~9はConfigurableJointを持っており、Segment1はHeadに、Segment2はSegment1に...といった具合に鎖状につながっています。
コライダーはHeadだけCapsuleCollider、他はBoxColliderとしました。

図3

HeadとSegment1~8はスクリプトを持っておらず、Segment9にだけ下記のようなスクリプトをアタッチしました。
CapsuleController2targetAngularVelocityを使ってモーターの速度を設定する方式なのに対し、こちらはtargetRotationを使って目標角度を与える方式になっているという差異はありますが、セグメント間のジョイントが動力になっているという点は同様です。

using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(ConfigurableJoint))]
public class WormController : MonoBehaviour
{
    [SerializeField] private float omegaInDegrees = 90.0f;
    [SerializeField] private float timeOffset = 0.5f;
    [SerializeField] private float spring = 500.0f;
    [SerializeField] private float damper = 100.0f;
    [SerializeField] private float angularLimitInDegrees = 60.0f;

    private readonly List<ConfigurableJoint> joints = new List<ConfigurableJoint>();
    private float time;

    private void Start()
    {
        var j = this.GetComponent<ConfigurableJoint>();
        do
        {
            j.xMotion = ConfigurableJointMotion.Locked;
            j.yMotion = ConfigurableJointMotion.Locked;
            j.zMotion = ConfigurableJointMotion.Locked;
            j.angularXMotion = ConfigurableJointMotion.Free;
            j.angularYMotion = ConfigurableJointMotion.Locked;
            j.angularZMotion = ConfigurableJointMotion.Locked;
            j.angularXDrive = new JointDrive
            {
                maximumForce = float.MaxValue,
                positionSpring = this.spring,
                positionDamper = this.damper
            };
            this.joints.Add(j);
            j = j.connectedBody.GetComponent<ConfigurableJoint>();
        } while (j != null);
    }

    private void FixedUpdate()
    {
        for (var i = 0; i < this.joints.Count; i++)
        {
            var j = this.joints[i];
            var t = Mathf.Max(this.time - (i * this.timeOffset), 0.0f);
            var angle = this.angularLimitInDegrees * Mathf.Sin(t * this.omegaInDegrees * Mathf.Deg2Rad);
            j.targetRotation = Quaternion.Euler(angle, 0.0f, 0.0f);
        }

        this.time += Time.deltaTime;
    }
}

実行してみると、おぞましい化け物のような動きをしました...

図4

追記: targetRotationによるカプセル操作

using UnityEngine;

[RequireComponent(typeof(ConfigurableJoint))]
public class CapsuleController3 : MonoBehaviour
{
    [SerializeField] private float angularSpeedInDegrees = 120.0f;
    [SerializeField] private float spring = 500.0f;
    [SerializeField] private float damper = 100.0f;
    [SerializeField] private float angularLimitInDegrees = 60.0f;

    private ConfigurableJoint configurableJoint;
    private float angle;

    private void Start()
    {
        this.configurableJoint = this.GetComponent<ConfigurableJoint>();
        this.configurableJoint.xMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.yMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.zMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.angularXMotion = ConfigurableJointMotion.Free;
        this.configurableJoint.angularYMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.angularZMotion = ConfigurableJointMotion.Locked;
        this.configurableJoint.angularXDrive = new JointDrive
        {
            maximumForce = float.MaxValue,
            positionSpring = this.spring,
            positionDamper = this.damper
        };
    }

    private void Update()
    {
        var input = Input.GetAxis("Vertical");
        this.angle = Mathf.Clamp(
            this.angle + (this.angularSpeedInDegrees * Time.deltaTime * input),
            -this.angularLimitInDegrees,
            this.angularLimitInDegrees);
        this.configurableJoint.targetRotation = Quaternion.Euler(this.angle, 0.0f, 0.0f);
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/11/13 07:31

    追加情報ありがとうございます。ご質問者さんの考える動きが次第に見えてきたように感じますが、まだ意図を取り違えているかもしれませんので気になる点がありましたらお気軽にコメントください。
    親のローカル空間における位置は固定して回転は許す...といった拘束を行うならConfigurableJoint、特に回転軸が1本ならば、もっとシンプルに操作できるHingeJointでいいんじゃないかと思いまして試してみました。
    投稿字数が尽きてしまい、別回答に移りますがご容赦ください...

    キャンセル

  • 2020/11/13 10:34

    ご検討いただきありがとうございます。これからソース確認させていただきます。
    実はご検討いただいているのは、内視鏡の先端部になります。↓このようなイメージです。
    https://www.thanko.jp/shopdetail/000000003306/
    ぷるぷる震えてしまうのはリアルタイムで物理計算させているので、どうしても起きてしまうものかなと思いました。
    Unityで用意されているロボットアーム『Articulation Body』も検討したことがあります。
    https://blogs.unity3d.com/jp/2020/05/20/use-articulation-bodies-to-easily-prototype-industrial-designs-with-realistic-motion-and-behavior/
    これもピタッと止まることはなく、少し行き過ぎてしまう傾向にあります。(だいぶ動きはいいのですが・・・)
    また持ち上がる動きも試してみたのですが、トルクをかけてないためか、動きは再現できませんでした。
    もしご興味いただければArticulation Bodyご確認いただけると幸いです。サンプルプログラムもGitHubにあげられてます。

    キャンセル

  • 2020/11/16 07:23 編集

    ご紹介ありがとうございます。ArticulationBodyを試してみましたので、またもや別回答になりすみませんが追記しました。ですがご質問者さんの希望されるピタッと止まる動きは実現しきれず、大してお役に立てる情報ではないかもしれません。

    ArticulationBodyは初挑戦なのですが、これもなかなか面白そうなものですね。回答の末尾にも少し申し上げましたが、ArticulationBodyとRigidbodyを連結するにはどうするのがいいんでしょうかね?
    手軽に繋ぐことができれば、部分的にArticulationBodyを使う...つまり従来のRigidbodyのキャラクターにArticulationBodyの腕を生やしたりできそうで興味深いです。今後のアップデートに期待するところでしょうかね。

    (追記: https://cginterest.com/2020/09/17/unity-2020-2-%E3%83%99%E3%83%BC%E3%82%BF%E7%89%88%E3%81%8C%E5%88%A9%E7%94%A8%E5%8F%AF%E8%83%BD%E3%81%AB%EF%BC%81/ によると、2020.2なら可能?)

    キャンセル

checkベストアンサー

+1

ArticulationBodyを試してみました。
コメント欄でご紹介いただいた内視鏡をイメージして、下記のように役割分担しています。

  • Root...アーティキュレーションのルートとして空のオブジェクトにArticulationBodyをアタッチしただけのもの。
  • Tail...青い直方体。タイプはPrismaticJointで、体軸方向への平行移動だけを許可する。内視鏡を配管内へ挿入する腕のつもり。
  • PassiveSegment...グレーのカプセル。タイプはSphericalJointで、±90°までスイングできる。スイングのstiffnessは0、dampingはある程度大きな値とし、これを鎖状に繋いである程度の固さを持つ紐として振る舞わせる。自由にツイスト可能とするが、ツイストのstiffnessは非常に大きな値、dampingはかなり大きな値とする(ツイストをLockedMotionにして完全に回転を禁止しようとすると、無理にねじれを解消しようとした結果なのか暴れ回ることがあったため、多少のねじれは許容するようにした)。内視鏡のコードのつもり。
  • ActiveSegment...黄色のカプセル。タイプはSphericalJointで、±後述の回転角上限÷セグメント数°までスイングできる。stiffnessは非常に大きな値として目標角度まで速やかに回転しつつ、dampingもかなり大きな値としてオーバーシュートがなるべく起こらないようにした。stiffnessが大きいので重力などの外力ではへたれにくく、曲がった形を維持できる。目標角度への追従性が高いため、目標角度は急激に変化させずゆったりと動かすことでそれらしい挙動になると思われる。とはいえ、PassiveSegmentのたわみなどのせいで内視鏡全体としてはいくらかのバネ運動をしてしまうようだった。また、根元のセグメントだけはツイスト可能とし、目標角度操作によって体軸周りに回転できるようにした。内視鏡先端の可動部分のつもり。
  • Head...赤いシリンダー。タイプはFixedJointで、ActiveSegmentの末端に固定された状態にある。後述のEndoscopeControllerがアタッチされている。動作上必須なパーツではないが、アーティキュレーションの動作制御役として具体的な形状を与えたくなったためこのようにした。おまけとしてカメラを搭載しており、内視鏡の映像を見ることができる。

各部位のArticulationBodyの設定は、シーン編集時点ではアンカーの位置・向きを調整する程度にとどめ(今回の場合はアンカーのX軸を体軸方向であるローカルZ軸方向に向けています)、各種物理特性はHeadにアタッチしたEndoscopeControllerで設定させました。

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class EndoscopeController : MonoBehaviour
{
    [SerializeField] private float linearSpeed = 5.0f;
    [SerializeField] private float angleLimit = 150.0f;
    [SerializeField] private float angularSpeed = 60.0f;
    [SerializeField] private float activeSegmentStiffness = 1e+07f;
    [SerializeField] private float activeSegmentDamping = 1e+05f;
    [SerializeField] private float passiveSegmentSwingDamping = 1e+04f;
    [SerializeField] private float passiveSegmentTwistStiffness = 1e+07f;
    [SerializeField] private float passiveSegmentTwistDamping = 1e+05f;
    [SerializeField] private float tailDamping = 1e+04f;
    [SerializeField] private bool lockPassiveSegmentSwingH;
    [SerializeField] private bool lockPassiveSegmentSwingV;
    [SerializeField] private bool lockPassiveSegmentTwist;
    [SerializeField] private float headMass = 0.1f;
    [SerializeField] private float activeSegmentMass = 0.1f;
    [SerializeField] private float passiveSegmentMass = 1.0f;
    [SerializeField] private float tailMass = 2.0f;
    [SerializeField] private string twistInputName = "Horizontal";
    [SerializeField] private string linearInputName = "Vertical";
    [SerializeField] private string swingInputXName = "Horizontal2";
    [SerializeField] private string swingInputYName = "Vertical2";

    // 各部位
    private ArticulationBody head;
    private readonly List<ArticulationBody> activeSegments = new List<ArticulationBody>();
    private ArticulationBody lastActiveSegment;
    private readonly List<ArticulationBody> passiveSegments = new List<ArticulationBody>();
    private ArticulationBody tail;
    private ArticulationBody root;

    // 目標角度
    private Vector2 targetSwingAngle;
    private float targetTwistAngle;

    private void Awake()
    {
        // まず各部位を取得する
        this.head = this.GetComponent<ArticulationBody>();
        var nextBody = this.head.transform.parent.GetComponent<ArticulationBody>();
        while (nextBody.name.Contains("Active"))
        {
            this.activeSegments.Add(nextBody);
            nextBody = nextBody.transform.parent.GetComponent<ArticulationBody>();
        }
        this.lastActiveSegment = this.activeSegments.Last();
        while (nextBody.name.Contains("Passive"))
        {
            this.passiveSegments.Add(nextBody);
            nextBody = nextBody.transform.parent.GetComponent<ArticulationBody>();
        }
        this.tail = nextBody;
        this.root = this.tail.transform.parent.GetComponent<ArticulationBody>();
        Debug.Log($"Active segments: {this.activeSegments.Count}, Passive segments: {this.passiveSegments.Count}");

        // 頭の設定
        this.head.mass = this.headMass;
        this.head.jointType = ArticulationJointType.FixedJoint;

        // アクティブセグメントの設定
        var angleLimitOfSegment = this.angleLimit / this.activeSegments.Count;
        foreach (var segment in this.activeSegments)
        {
            segment.mass = this.activeSegmentMass;
            segment.jointType = ArticulationJointType.SphericalJoint;
            segment.swingYLock = segment.swingZLock = ArticulationDofLock.LimitedMotion;
            segment.twistLock = ArticulationDofLock.LockedMotion;
            segment.yDrive = segment.zDrive = new ArticulationDrive
            {
                upperLimit = angleLimitOfSegment,
                lowerLimit = -angleLimitOfSegment,
                stiffness = this.activeSegmentStiffness,
                damping = this.activeSegmentDamping,
                forceLimit = float.MaxValue
            };
        }

        // 根元のアクティブセグメントだけ設定を一部変える
        this.lastActiveSegment.twistLock = ArticulationDofLock.FreeMotion;
        this.lastActiveSegment.xDrive = new ArticulationDrive
        {
            stiffness = this.activeSegmentStiffness,
            damping = this.activeSegmentDamping,
            forceLimit = float.MaxValue
        };

        // パッシブセグメントの設定
        foreach (var segment in this.passiveSegments)
        {
            segment.mass = this.passiveSegmentMass;
            segment.jointType = ArticulationJointType.SphericalJoint;
            segment.swingYLock = this.lockPassiveSegmentSwingH
                ? ArticulationDofLock.LockedMotion
                : ArticulationDofLock.LimitedMotion;
            segment.swingZLock = this.lockPassiveSegmentSwingV
                ? ArticulationDofLock.LockedMotion
                : ArticulationDofLock.LimitedMotion;
            segment.twistLock = this.lockPassiveSegmentTwist
                ? ArticulationDofLock.LockedMotion
                : ArticulationDofLock.FreeMotion;
            segment.xDrive = new ArticulationDrive
            {
                stiffness = this.passiveSegmentTwistStiffness,
                damping = this.passiveSegmentTwistDamping,
                forceLimit = float.MaxValue
            };
            segment.yDrive = segment.zDrive = new ArticulationDrive
            {
                upperLimit = 90.0f,
                lowerLimit = -90.0f,
                damping = this.passiveSegmentSwingDamping,
                forceLimit = float.MaxValue
            };
        }

        // 尻尾の設定
        this.tail.mass = this.tailMass;
        this.tail.jointType = ArticulationJointType.PrismaticJoint;
        this.tail.linearLockX = ArticulationDofLock.FreeMotion;
        this.tail.linearLockY = this.tail.linearLockZ = ArticulationDofLock.LockedMotion;
        this.tail.xDrive = new ArticulationDrive
        {
            damping = this.tailDamping,
            forceLimit = float.MaxValue
        };

        // ルートの設定
        this.root.useGravity = false;
        this.root.immovable = true;
    }

    private void FixedUpdate()
    {
        // アクティブセグメントの屈曲
        var swingInput = new Vector2(
            Input.GetAxis(this.swingInputXName),
            Input.GetAxis(this.swingInputYName));
        this.targetSwingAngle += Time.deltaTime * this.angularSpeed * swingInput;
        this.targetSwingAngle.x = Mathf.Clamp(this.targetSwingAngle.x, -this.angleLimit, this.angleLimit);
        this.targetSwingAngle.y = Mathf.Clamp(this.targetSwingAngle.y, -this.angleLimit, this.angleLimit);
        var targetAngleOfSegment = this.targetSwingAngle / this.activeSegments.Count;
        foreach (var segment in this.activeSegments)
        {
            var segmentDrive = segment.yDrive;
            segmentDrive.target = targetAngleOfSegment.x;
            segment.yDrive = segmentDrive;
            segmentDrive.target = targetAngleOfSegment.y;
            segment.zDrive = segmentDrive;
        }

        // アクティブセグメントのひねり
        var twistInput = Input.GetAxis(this.twistInputName);
        this.targetTwistAngle += Time.deltaTime * this.angularSpeed * twistInput;
        var lastActiveSegmentDrive = this.lastActiveSegment.xDrive;
        lastActiveSegmentDrive.target = this.targetTwistAngle;
        this.lastActiveSegment.xDrive = lastActiveSegmentDrive;

        // 尻尾の押し引き
        var linearInput = Input.GetAxis(this.linearInputName);
        var tailDrive = this.tail.xDrive;
        tailDrive.targetVelocity = linearInput * this.linearSpeed;
        this.tail.xDrive = tailDrive;
    }
}

ヒエラルキーは壮絶な様相を呈しています。

図1

操作方法としては、左スティック上下で尻尾を押し引きする、左スティック左右で可動部をひねる、右スティックで可動部を上下左右に屈曲させる...といったもの想定してみました。
頭部を動かしてみましたが、今のところ操作をやめるとピタッと静止するという風にはできておらず、若干揺れています。もしかすると適当なところで妥協した方がいいかもしれません。静止させるために過剰な拘束を行うと、拘束に反する力がかかった時にそれを解消しようとして大暴れするようなこともありました...

図2

せっかくなので迷路の中を探検させてみましたが、コードが短くてあまり奥までは進めません。アーティキュレーションは最大64連結までの制限があるらしいので、あまり長くはできないかもしれませんね。

図3

コード部分を通常のRigidbodyの鎖にするような方針もあり得るかもしれませんが、RigidbodyArticulationBodyを手軽に接続できるジョイントみたいなものは見当たらず、ちょっと手間がかかりそうです。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/11/19 22:08

    ご対応いただきありがとうございます。またご連絡遅れて申し訳ございません。
    本物の内視鏡の動きにだいぶ近づいてきています。
    また、いただいたやり方で試そうとしているのですが、ゲームコントローラの入力がうまく入らない状態です。

    ・各関節に設けられているカプセルはどういった意味があるのでしょうか。
    ・Headのみにスクリプトをアタッチするということでよろしかったでしょうか。
    ・根本のTail(青カプセル)が重力で落ちないのですが、そういった設定でしょうか。

    今回のご対応でうまく動きましたら、ベストアンサーとさせていただきたいと思います。

    キャンセル

  • 2020/11/19 22:28

    関節が64個までだとやはり長さの問題がありますね・・・実際はかなり長いもの(10m~30mもある)ので、関節数はもう少し増やしておきたいところです。

    また自分が以前検討していた別の方法ですが、Articulationを使わない方法で考えてました。
    ・関節曲げトルクには最高角速度を設定。ピタ止めするため、必要以上にスピードが出ないようにする
    ・ゲームコントローラの入力あり⇒速度制限ありのトルクをかけて関節を曲げる。
    ・ゲームコントローラの入力なし+関節に角速度が残っているとき⇒角速度ゼロまで、マイナス側にトルクをかける(ブレーキの役目)
    ⇒これだけだと計算が間に合わず、反対側に関節が行き過ぎてしまうためさらに↓
    ・ローカル角度は、任意の角度位置をターゲットに止めるようにする。
    ⇒例えば、30.3164…° の位置でトルクをかけるのを止めたとします(ゲームコントローラ入力ゼロ)
    ⇒マイナス側にトルクを働かせる(ブレーキの役目)、
    ⇒ローカル角速度がゼロに近づいたら、たとえば近い角度のきりのいい位置、例えば、31°の位置を読み取って、強制的に位置固定させる。

    みたいなことを考えてます。実際のロボットアームに使われるモーターの考え方に似ています。Unityの場合、物理計算が間に合ってくれるかどうかがあるため、角速度の速度規制とマイナストルク(ブレーキ)の調整がポイントになるかなと思ってます。
     ConfigJointの接続だけだと、持ち上がりの動きができないため、各関節の重心を思いっきり後ろにもってます。ワイヤーで引っ張っているイメージに近いです。そうすれば持ち上がりの動きを作ることができます。

    この内容はご興味いただいたらのご対応でかまいません。必要であれば別質問で立ち上げさせていただきます。

    キャンセル

  • 2020/11/20 05:58

    各関節のカプセルですが、あれらはメニューから作成できるただのCapsuleCollider付きカプセルです。役割としては画面上で目に見える形を与えること(MeshFilter、MeshRenderer)とArticulationBodyの物理的形状を与えること(CapsuleCollider)ですかね?
    カプセル自体にArticulationBodyをアタッチして階層構造を作ってもかまわなかったのですが、あんな風にカプセルだけArticulationBodyの子階層にぶら下げた理由としては、カプセルの向き(あるいはスケールも)をUnity上で調整したかったから...というのがありますね。
    今回はカプセルをZ軸方向に並べて鎖状にしましたが(これも半ば恣意的に決めたもので、「Unityの慣習としては前後といったらZ軸だろう」みたいなイメージからです)、カプセルのモデルはY軸を向いた形に作られていますので回転させてやる必要がありました。そこでArticulationBodyの子として別オブジェクトにすることで、ArticulationBodyの向きはそのままにカプセルの向きだけ回せるようにしたわけです。

    「Headのみにスクリプトをアタッチする」ということについてはおっしゃる通りです。他の部位にはスクリプトを付けておりません。
    これもやはりこうである理由は特になく、自由な設計でかまわないと思います。ご質問者さんからご紹介いただいたArticulationBodyのサンプルプロジェクトに収録されていたロボットアームの場合は、各ArticulationBodyを直接に操作する役割はそれぞれの部位にアタッチされたスクリプトが担当しているようでした。

    Tailが落下しないというのは、おっしゃる通りそういう仕様です...
    今回の実験では、ルートオブジェクトを

    // ルートの設定
    this.root.useGravity = false;
    this.root.immovable = true;

    という風に空間中に固定してしまいました。実は当初はこのルートオブジェクトはなく、Tailがルートで重力も作用させ、空中なら落下するような仕様で作り始めたのです。
    内視鏡の挿入・引き出しはTailに対するAddForceで行おうと思ったのですが、なぜかこれだと謎の大暴れ現象の発生率が高く、試しに現在のPrismaticJointでスライドさせる方式にしたところ安定したため「アーティキュレーション全体の自由な運動は今回の主眼じゃないだろうからこれでいいか」と妥協したものです。
    あの大暴れが仕方ないものなのか、やりようがまずかったのかは未調査です。内視鏡全体がワールド空間中で自由に運動できるようにしないとまずいようなら、この辺も調査しないとならなそうですね...

    かなり長い内視鏡を作るとなると、さらにハードルが上がりそうですね...ご紹介いただいたUnity Blogの記事でも、

    従来のジョイントを使用することの問題点

    第 1 の問題は、ソルバーに収束の問題を引き起こす可能性のある相反する要因の数が非常に多いことです。反復回数、接続された物体の相対的な質量、シーン内の制約のセットの全体の複雑度によって、解決不能なシナリオが作成されることがあります。このような場合、部分解が使用されるため、一部の制約が満たされないままになってしまいます。

    第 2 の問題は、適用されるインパルスの大きさが、特定の時間に制約がどの程度破られているかを示す値である、ジョイントの誤差に依存していることです。この誤差補正の動作のため、特にジョイントが連結されている場合には、ボディが減衰スプリングのセットで連結されているような、ばねのような効果が常にいくらか発生します。

    なんてことが挙げられていますね。内視鏡のコード部分は地形に沿って屈曲するので、やはり大量のオブジェクトが数珠繋ぎになった構造になってしまうでしょう。ですので記事で言うところの「シーン内の制約のセットの全体の複雑度」とか「ジョイントの誤差」だとかの要因がすごく効いてきそうな気がします。

    見えないワイヤーで引っ張る設計もよさそうな気がしますね。私のような素人目から見るかぎりでは妥当な理屈のように感じます。
    とはいえ今回のご質問に取り組んで、多数連結された物体のシミュレーションはかなりやっかいなのを体験した上では、果たしてどんな挙動をするか予想できないですね(もはやUnityの物理シミュレーションには頼らず、外部GPUをつないで計算するような高精度のシミュレーションシステムを作らないといけない領域なのかも)...

    キャンセル

  • 2020/11/20 09:04

    ご回答いただきありがとうございます。もう少しこちらのほうでも試してみます。

    また過去の自分の検討では、ジョイントは200連結(2m)相当まで、動作させることはできました。GPU積んだパソコンで、Physx3時代(Unity2017)、それほど高スペックPCでなくても動かすことはできました。
    ・コードの部分は親子関係を作らず、ConfigJointで接続。(曲げ操作はないので)
    ・先端の曲げ・前後の挿入スピードをゆっくりにする
    ・挿入対称側(配管で実施)にRigidbodyを入れない(コライダーのみ)
    ・内視鏡直径は1mで。(実寸にしない)⇒得意なスケールがあるみたいです。
    などの工夫を入れれば、動きました。

    先述させていただいた、マイナストルク(ブレーキ)をかける方法ですが、こちらもすでに検討済でかなり近いところまで来ています。親子関係を作って、ローカル座標ストップを実施してみたいと思います。
     また新たに質問させていただきながら検討の途中結果を報告させていただけたらと思います。

    キャンセル

0

ヒンジジョイントではどうなるか試してみました。
実験に使ったオブジェクトはメニューから作成できる単なるSphereで、それらを横に4個並べてそれぞれRigidbodyHingeJoint、後述のArmSegmentスクリプトをアタッチしています。RigidbodyHingeJointはアタッチしたての状態のままで、ジョイントの設定はスクリプト内で行いました。
また、ご質問者さんのスクリーンショットにならって親、子、孫、ひ孫の階層構造にしました。親には青いマテリアル、子には黄色いマテリアルを割り当てましたが、それらマテリアルの柄をビーチボールみたいな市松模様にして回転状態を見やすくしてみようと思いました。

ArmSegmentは下記のようになっています。インスペクター上で(または別のスクリプトから)targetAngleを操作すると、それに向かってヒンジの角度を調整しようとします。

using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

[RequireComponent(typeof(Rigidbody), typeof(HingeJoint))]
public class ArmSegment : MonoBehaviour
{
    // 目標角度
    public float targetAngle;

    // ヒンジの軸の向き
    [SerializeField] private Vector3 axis = Vector3.forward;

    // オフならヒンジのspring、damper、targetPositionでコントロールし
    // オンならgain、derivativeTime、integrationTimeでコントロールする
    [SerializeField] private bool usePidControl;

    [Header("Spring-Damper Control")]

    // ヒンジジョイントのバネ強度
    [SerializeField] private float spring = 100000.0f;

    // ヒンジジョイントのダンパー強度
    [SerializeField] private float damper = 10000.0f;

    [Header("PID Control")]

    // 目標角からの偏差に比例したトルクをかける際の比例定数
    [SerializeField] private float gain = 1000.0f;

    // 微分項の効きの強さ
    [SerializeField] private float derivativeTime = 0.2f;

    // 積分項の効きの弱さ
    [SerializeField] private float integrationTime = 100.0f;

    // 前回のFixedUpdate時の目標角からの偏差...偏差変化率の算出に使う
    // targetAngleが大きく変更された、あるいはアームに衝撃が加わって
    // 角度が目標角から急激にずれたときにこの値が変動する
    // derivativeTimeを上げるとずれを補正する追加のトルクがかかる
    private float previousDeltaAngle;

    // 目標角からの偏差を積算したもの
    // もしこれがゼロからずれているときは、長時間にわたって目標角度を超えたまま、
    // あるいは足りないままであると考えられる
    // integrationTimeを下げるとずれを補正する追加のトルクがかかる
    private float integratedDeltaAngle;

    private ArmSegment parent;
    private new Rigidbody rigidbody;
    private HingeJoint joint;

    private void Awake()
    {
        // ヒンジのanchorはVector3.zeroとし、ボールの中心を回転軸とする
        var p = this.transform.parent;
        this.parent = p == null ? null : p.GetComponent<ArmSegment>();
        this.rigidbody = this.GetComponent<Rigidbody>();
        this.joint = this.GetComponent<HingeJoint>();
        this.joint.autoConfigureConnectedAnchor = true;
        this.joint.axis = this.axis;
        this.joint.anchor = Vector3.zero;
    }

    private void Start()
    {
        // ひ孫のヒンジを孫のRigidbodyに、孫のヒンジを子のRigidbodyに、
        // 子のヒンジを親のRigidbodyに繋ぎ、親のヒンジはワールド空間に繋ぐ
        this.joint.connectedBody = this.parent == null ? null : this.parent.rigidbody;
    }

    private void FixedUpdate()
    {
        this.joint.useSpring = !this.usePidControl;
        if (this.usePidControl)
        {
            // AddRelativeTorqueを使う場合
            var deltaAngle = this.targetAngle - this.joint.angle;
            this.integratedDeltaAngle += deltaAngle;
            var deltaAngleVelocity = (deltaAngle - this.previousDeltaAngle) / Time.deltaTime;
            var control = Mathf.Deg2Rad * this.gain * (
                deltaAngle +
                (this.integratedDeltaAngle / this.integrationTime) +
                (deltaAngleVelocity * this.derivativeTime));
            this.rigidbody.AddRelativeTorque(this.joint.axis * control);
            this.previousDeltaAngle = deltaAngle;
        }
        else
        {
            // ヒンジのバネを使う場合
            this.joint.spring = new JointSpring
            {
                spring = this.spring,
                damper = this.damper,
                targetPosition = this.targetAngle
            };
        }
    }

    #if UNITY_EDITOR
    private void OnDrawGizmos()
    {
        if (this.joint == null)
        {
            return;
        }

        Handles.Label(this.transform.position, $"T: {this.targetAngle:F1}\nA: {this.joint.angle:F1}");
    }
    #endif
}

各部位の角度としてはHingeJoint.angleを使用しました。ご質問者さんのおっしゃる通り、親子関係を持たない場合はTransformから相対角度を求めるのに座標系変換が必要で一手間かかるでしょう。今回はご質問者さんのやり方に沿って親子関係を構築しましたが、ジョイントによってはこういった手軽な方法で角度を得られますので、親子関係を作らない場合でもそれらに頼ればさほど面倒はないように思います。

ヒンジジョイントにも駆動機構がありますので、まずはそれを使って制御させてみました。usePidControlをオフにするとそのような動作になります。
ヒエラルキー上の4つのオブジェクトを全部選択して、インスペクターでtargetAngleを一斉に操作したところ下図のようになりました。

図1

一方、ご質問者さんが想定しているようなトルクを加える方法だと、トルクの大きさを自前で調整してやる必要があるでしょう。usePidControlをオンにすると下図のようになりました。

図2

PID制御 - Wikipedia」の記事をまねてみたつもりですが、ご覧の通り追従性が悪く、動きがプルプルしています...
以前申し上げたようにロボットとか制御工学は無知でして、計算が間違っているかパラメーター調整が甘いか、あるいは物理シミュレーション上の制限...タイムステップが長すぎるとかの原因があるのだろうと思います。まあポンコツロボットみたいでかわいいので、表現上面白い使い道があるかもしれませんが...

自前でトルクをかける方法は、Unity組み込みのヒンジ駆動機構より自由度が高く調整の余地があると言えるでしょう。おそらくご質問者さんの方がこういった制御についてお詳しいように思いますので、うまくやれば安定性と目標角度到達性を両立できるかもしれません。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.78%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る