投稿記事

おひのっと ohinot 2024/05/17 01:17

おひのっと ohinot 2024/03/07 00:37

ゲームキャラの感情の表現例

ゲームのキャラクターの感情を表現したいです。
どのようにすれば人間らしい感情を表現できるでしょうか。

内的感情(ラッセルの円環モデル)

人間の感情をいくつかの要素に分けて評価・分類する手法はいろいろあるようです。
よく引用されるものの一つに「ラッセルの円環モデル」と呼ばれるものがあります。

ラッセルさんは、すべての感情を「快-不快」「覚醒-非覚醒」という2つの軸で評価すると円環状に配置される傾向があり、感情の種類と強度はベクトルの向きと長さで表されるとしています。
快-不快軸は文字通り、その感情が人間にとって心地よいものかどうかを評価しています。
覚醒-非覚醒軸は少し分かりにくいですが、およそ活動的かどうかを評価しているようです。
快・覚醒方向には「愉快」「幸福」といった類似した感情が位置し、反対の不快・非覚醒方向には「退屈」「憂鬱」といった反対の感情が位置します。
右上から反時計回りに「喜・怒・哀・楽」が対応すると説明されることもあるようです。

James A Russell: A Circumplex Model of Affect
https://www.researchgate.net/publication/235361517_A_Circumplex_Model_of_Affect
図お借り 情動評価のためのラッセルの円環モデルに基づく感情重心推定手法の提案
https://www.jstage.jst.go.jp/article/jjske/18/3/18_TJSKE-D-18-00069/_article/-char/ja/

たとえばキャラクターに「快-不快」「覚醒-非覚醒」という変数を持たせ、その値をキャラクターの表情やイベントの成否判定に利用し、イベントの成否に応じてまたその値を変動させる、といった方法が考えられるでしょう。
キャラクターの気分を表すこの変数を内的感情と呼ぶことにします。

対人感情(好き-嫌い・強気-弱気)

感情には対象となる人物がいる場合があります。
PCとNPCまたはNPC同士の間にそれぞれイベントが発生する場合、NPCは内的感情とは別に、PCまたはNPCそれぞれに対する感情を持つ必要があります。
これを対人感情と呼ぶことにします。

ゲームではよくPCとNPCの関係を表す値として好感度や親密度といった値が用いられます。
好感度の高い相手とは好意的なイベントが発生しやすく、好感度の低い相手とは険悪なイベントが発生しやすい、といったものです。

しかし互いに好意(または悪意)をもっていてもその関係は対等とは限りません。
たとえば仲の良い兄弟がいたとして、弟を可愛い、守りたいと思う兄の立場は弟より強く、兄を信頼し、また憧れを持つ弟の立場は兄より弱いと言えます。
「怒り」と「恐れ」はラッセルの円環モデルでは類似した感情ですが、立場の強弱に注目してみると、「怒り」には相手の立場を低く見積もる視点があり、「恐れ」には相手の立場を高く見積もる視点があり、区別できることが分かります。

対人感情をラッセルの円環モデルに倣い「好き-嫌い」「強気-弱気」という2つの軸で表現し、感情の例を配置してみました。

対人感情は、内的感情と比較して、その変化がゆるやかであるように思います。

感情表現

感情と表情は密接に関係しています。
幸せを感じれば自然と笑顔になりますし、苦痛を抱えながら朗らかな笑顔を作るのは難しいでしょう。

内的感情と対人感情の快軸と好き軸、覚醒軸と強気軸をそれぞれ平均したとして、表情を次のように割り当ててみました。

快+好きであるほど笑顔に。
不快+嫌いであるほど嫌そうな顔に。
覚醒+強気であるほど目に自信を、口を大きく。
非覚醒+弱気であるほど目に自信を無くし、口を小さく。
ややマンガ的な表現ではありますが、おおよそラッセルの円環モデルと一致し、喜・怒・哀・楽を表現できているのではないかと思います。

しかしまた表情はコミュニケーションの手段であり、本当の感情と必ずしも一致するものではないでしょう。
表情には、それを相手に見せどう思わせたいか、という意図が含まれている場合があります。
楽しくなくても笑顔を作ることはありますし、不快だからといってところ構わず不快な顔をするわけにはいきません。
状況やそのキャラクターがとっている行動に合わせた表情、すなわち演技的な感情を、ここでは演技感情と呼ぶことにします。

演技感情は口元に現れやすく、内的感情や対人感情は目元に現れやすいと考え、表情のバリエーションを次のように割り当ててみました。

この表情のバリエーションをキャラモデルに組み込んだ例です。

まとめ

ゲームのキャラクターの感情を表現する方法について考えてみました。
恋をしてときめいているときの表情や、性的に感じているときの表情などをどのように表現したらいいかは、また今度考えてみたいと思います。

おひのっと ohinot 2024/02/19 16:02

【Unity】複数の回転、移動、スケール、LookAtコンストレイントを順番に適用するスクリプト

複数の回転、移動、スケール、LookAtコンストレイントを順番に適用するスクリプトです。
BlenderのCopy Rotation(回転コピー)、Copy Location(位置コピー)、Copy Scale(拡大縮小コピー)、Transformation(トランスフォーム変換)、Damped Track(減衰トラック)、Locked Track(軸固定トラック)となるべく同じように動作するように作りました。
読み込み時のトランスフォームの状態をレストポーズ(Blenderのポーズモードでトランスフォームがすべて0のときと同じような状態)として扱います。
Track To(上固定のトラック)をさせたい場合、上にしたい軸単体で照準軸をターゲットに向けたあと、残りの軸で照準軸をターゲットに向ければできると思います。
Unity 2020で動作確認していましたが、2022ではEditorまわりの挙動が変わっていたため折りたたみをなくしました。
利用・改変・転載ご自由に。

//MultiConstraint
//複数の回転、移動、スケール、LookAtコンストレイントを順番に適用するスクリプト
//利用・改変・転載ご自由に (CC0 https://creativecommons.org/publicdomain/zero/1.0/deed.ja)
//2024.05.24 Unity 2022 でUIが乱れていたので折り畳みなしに
//2024.05.23 Scale追加、sourceがnoneのとき入力値を適用するように
//2024.02.19 初版

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class MultiConstraint : MonoBehaviour
{
    [Range(0f, 1f)]
    [Tooltip("影響量。非球状補間。")]
    public float weight = 1f;
    //[Tooltip("読み込み時に自身とソースのTransformの値をレストポーズとして使用する。")]
    //[SerializeField] bool transformToRestpose = true;
    public Vector3 restPosition = Vector3.zero;
    public Quaternion restQuat = Quaternion.identity;
    public Vector3 restScale = Vector3.one;
    [SerializeField] public List<ConstraintData> constraintList = new List<ConstraintData> { new ConstraintData() };
    //int constraintListCount = 1;

    void Start() 
    {
        //レストポーズを記憶
        restPosition = transform.localPosition;
        restQuat = transform.localRotation;
        restScale = transform.localScale;

        for (int i = 0; i < constraintList.Count; i++)
        {
            if (constraintList[i].source &&
                (constraintList[i].constraintType == ConstraintData.ConstraintType.Position ||
                constraintList[i].constraintType == ConstraintData.ConstraintType.Rotation ||
                constraintList[i].constraintType == ConstraintData.ConstraintType.LookAt))
            {
                constraintList[i].sourceRestPosition = constraintList[i].source.localPosition;
                constraintList[i].sourceRestQuat = constraintList[i].source.localRotation;
            }
        }
    }

    void Update()
    {
        //静止時の回転と移動
        Quaternion copyQuat = Quaternion.identity;
        Vector3 copyEuler = Vector3.zero;
        Vector3 copyPosition = Vector3.zero;
        Vector3 copyScale = Vector3.one;

        //ソースごとに
        for (int i = 0; i < constraintList.Count; i++)
        {
            //有効でない、またはウェイトが0ならスキップ
            if ( !constraintList[i].isActive || constraintList[i].constraintWeight == 0f )
                continue;

            Quaternion transedQuat = Quaternion.identity;
            Vector3 transedEuler = Vector3.zero;
            Vector3 transedPosition;
            Vector3 transedScale;

            //Position、positionInputのとき
            if (constraintList[i].constraintType == ConstraintData.ConstraintType.Position)
            {
                //移動を取得
                //Self
                if (constraintList[i].space == Space.Self)
                {
                    if (constraintList[i].source)
                    {
                        //ソースのポーズ空間上の移動を得る
                        transedPosition =
                            Quaternion.Inverse(constraintList[i].sourceRestQuat) *
                            (constraintList[i].source.localPosition
                            - constraintList[i].sourceRestPosition);
                        //変換が必要なら変換
                        if (constraintList[i].transXTo != Vector3.right ||
                            constraintList[i].transYTo != Vector3.up ||
                            constraintList[i].transZTo != Vector3.forward)
                            transedPosition =
                                constraintList[i].transXTo * transedPosition.x
                                + constraintList[i].transYTo * transedPosition.y
                                + constraintList[i].transZTo * transedPosition.z;
                    }
                    //PositionInput
                    else
                        transedPosition = constraintList[i].positionInput;
                }
                //World
                else
                {
                    if (constraintList[i].source) 
                    {
                        //ソースのワールド座標を取得
                        transedPosition = constraintList[i].source.position;
                        //変換が必要なら変換
                        if (constraintList[i].transXTo != Vector3.right ||
                            constraintList[i].transYTo != Vector3.up ||
                            constraintList[i].transZTo != Vector3.forward)
                            transedPosition =
                                constraintList[i].transXTo * transedPosition.x
                                + constraintList[i].transYTo * transedPosition.y
                                + constraintList[i].transZTo * transedPosition.z;
                    }
                    //PositionInput
                    else
                        transedPosition = constraintList[i].positionInput;
                    //

                    //offsetならワールド移動をポーズの移動に変換
                    if (constraintList[i].offset)
                        transedPosition =
                            Quaternion.Inverse(restQuat) *
                            transform.parent.InverseTransformVector(transedPosition);
                    //非offsetならワールド座標をポーズの座標に変換
                    else
                        transedPosition =
                            Quaternion.Inverse(restQuat) *
                            (transform.parent.InverseTransformPoint(transedPosition) - restPosition);
                }

                //offset(加算)
                if (constraintList[i].offset)
                {
                    //補間して加算して保持
                    if (constraintList[i].constraintWeight != 1f)
                        transedPosition = Vector3.Lerp(
                            Vector3.zero,
                            transedPosition,
                            constraintList[i].constraintWeight);
                    copyPosition += transedPosition;
                }
                //非offset(置き換え)
                else
                {
                    //補間して置き換えて保持
                    if (constraintList[i].constraintWeight != 1f)
                        transedPosition = Vector3.Lerp(
                            copyPosition,
                            transedPosition,
                            constraintList[i].constraintWeight);
                    copyPosition = transedPosition;
                }
            }
            //Rotation、rotationInputのとき
            else if (constraintList[i].constraintType == ConstraintData.ConstraintType.Rotation)
            {
                //無変換のとき、Add以外ならEuler変換をパス、AddならEuler変換必要
                //変換が必要 (要Euler)
                //ConstraintWeightが1fでAddならQuaternion変換をパスできる 
                //ConstraintWeightが1fと0以外で Replaceのとき 要Quaternion(Replace以外は取得時に補間する)

                //Self && Add (Eulerで合成)
                if (constraintList[i].space == Space.Self
                    && constraintList[i].mix == ConstraintData.MixType.Add)
                {
                    //Rotation
                    if (constraintList[i].source)
                    {
                        //新しいローカル回転をQuaternionで取得
                        transedQuat =
                            Quaternion.Inverse(constraintList[i].sourceRestQuat) *
                            constraintList[i].source.localRotation;

                        //ウェイト補正
                        if (constraintList[i].constraintWeight != 1f)
                            transedQuat = Quaternion.Lerp(
                                Quaternion.identity,
                                transedQuat,
                                constraintList[i].constraintWeight);

                        //オイラー角に
                        transedEuler = EulerFixV3(transedQuat.eulerAngles);
                        transedQuat = Quaternion.identity;

                        //変換
                        if (constraintList[i].transXTo != Vector3.right ||
                            constraintList[i].transYTo != Vector3.up ||
                            constraintList[i].transZTo != Vector3.forward)
                            transedEuler = TransXYZ(i, transedQuat, transedEuler);
                    }
                    //RotationInput
                    else
                    {
                        if (constraintList[i].constraintWeight == 1f)
                            transedEuler = constraintList[i].rotationInput;
                        else
                            transedEuler = Quaternion.Lerp(
                                Quaternion.identity,
                                Quaternion.Euler(constraintList[i].rotationInput),
                                constraintList[i].constraintWeight
                            ).eulerAngles;
                        transedEuler = EulerFixV3(transedEuler);
                    }

                    //保持した回転がQuaternionならEulerに
                    if (copyQuat != Quaternion.identity)
                    {
                        copyEuler = copyQuat.eulerAngles;
                        copyQuat = Quaternion.identity;
                    }
                    //新しい回転をEulerに加算して保持
                    copyEuler += transedEuler;
                }
                //Self && AfterOriginal (Quaternionで合成)
                else if (constraintList[i].space == Space.Self
                    && constraintList[i].mix == ConstraintData.MixType.AfterOriginal)
                {
                    //Rotation
                    if (constraintList[i].source)
                    {
                        //新しいローカル回転をQuaternionで取得
                        transedQuat =
                            Quaternion.Inverse(constraintList[i].sourceRestQuat) *
                            constraintList[i].source.localRotation;

                        //新しい回転をウェイト補正
                        if (constraintList[i].constraintWeight != 1f)
                            transedQuat = Quaternion.Lerp(
                                Quaternion.identity,
                                transedQuat,
                                constraintList[i].constraintWeight);

                        //要変換なら一旦Eulerにして変換してQuaternionに戻す
                        if (constraintList[i].transXTo != Vector3.right ||
                            constraintList[i].transYTo != Vector3.up ||
                            constraintList[i].transZTo != Vector3.forward)
                            transedQuat = Quaternion.Euler(TransXYZ(i, transedQuat, transedEuler));
                    }
                    //RotationInput
                    else
                    {
                        if (constraintList[i].constraintWeight == 1f)
                            transedQuat = Quaternion.Euler(constraintList[i].rotationInput);
                        else
                            transedQuat = Quaternion.Lerp(
                                Quaternion.identity,
                                Quaternion.Euler(constraintList[i].rotationInput),
                                constraintList[i].constraintWeight);
                    }

                    //保持した回転がEulerならQuaternionに
                    if (copyEuler != Vector3.zero)
                    {
                        copyQuat = Quaternion.Euler(copyEuler);
                        copyEuler = Vector3.zero;
                    }
                    //回転を合成
                    copyQuat = copyQuat * transedQuat;
                }
                //Replace || World
                //ConstraintWeightが1なら新しい回転を保持(変換があったらEuler、なかったらQuaternion)
                //1以外なら回転をQuaternionにして元の回転と新しい回転を補間して保持
                else if (constraintList[i].space == Space.World
                    || constraintList[i].mix == ConstraintData.MixType.Replace)
                {
                    //Rotation
                    if (constraintList[i].source)
                    {
                        //Selfならターゲットのポーズ空間の回転を取得
                        if (constraintList[i].space == Space.Self)
                            transedQuat =
                                Quaternion.Inverse(constraintList[i].sourceRestQuat) *
                                constraintList[i].source.localRotation;
                        //Worldならターゲットのワールド回転を取得
                        else
                            transedQuat = constraintList[i].source.rotation;

                        //回転軸の変換が必要ならEulerで変換してQuaternionに
                        if (constraintList[i].transXTo != Vector3.right ||
                            constraintList[i].transYTo != Vector3.up ||
                            constraintList[i].transZTo != Vector3.forward)
                            transedQuat = Quaternion.Euler(TransXYZ(i, transedQuat, transedEuler));
                    }
                    //RotationInput
                    else
                        transedQuat = Quaternion.Euler(constraintList[i].rotationInput);

                    //ワールドの回転ならこのポーズ空間に変換
                    if (constraintList[i].space == Space.World)
                    {
                        if (transform.parent)
                            transedQuat = Quaternion.Inverse(transform.parent.rotation) * transedQuat;
                        transedQuat = Quaternion.Inverse(restQuat) * transedQuat;
                    }


                    //保持した回転がEulerならQuaternionに
                    if (copyEuler != Vector3.zero)
                    {
                        copyQuat = Quaternion.Euler(copyEuler);
                        copyEuler = Vector3.zero;
                    }

                    //保持した回転との間でウェイト補正
                    if (constraintList[i].constraintWeight != 1f)
                        transedQuat = Quaternion.Lerp(
                            copyQuat,
                            transedQuat,
                            constraintList[i].constraintWeight);

                    //Quaternionで保持
                    copyQuat = transedQuat;
                }
            }
            //Scaleのとき
            else if (constraintList[i].constraintType == ConstraintData.ConstraintType.Scale)
            {
                //拡大縮小を取得
                //Self
                if (constraintList[i].space == Space.Self)
                {
                    if (constraintList[i].source)
                    {
                        //ソースのポーズ空間上のスケールを得る
                        transedScale = constraintList[i].source.localScale;

                        //変換が必要なら変換
                        if (constraintList[i].transXTo != Vector3.right ||
                            constraintList[i].transYTo != Vector3.up ||
                            constraintList[i].transZTo != Vector3.forward)
                            transedScale =
                                constraintList[i].transXTo * transedScale.x
                                + constraintList[i].transYTo * transedScale.y
                                + constraintList[i].transZTo * transedScale.z;
                    }

                    //ScaleInput
                    else
                        transedScale = constraintList[i].scaleInput;
                }
                //World
                else
                {
                    if (constraintList[i].source) { 
                        //ソースのワールドスケースを得る
                        transedScale = constraintList[i].source.lossyScale;
                        //変換が必要なら変換
                        if (constraintList[i].transXTo != Vector3.right ||
                            constraintList[i].transYTo != Vector3.up ||
                            constraintList[i].transZTo != Vector3.forward)
                            transedScale =
                                constraintList[i].transXTo * transedScale.x
                                + constraintList[i].transYTo * transedScale.y
                                + constraintList[i].transZTo * transedScale.z;
                    }
                    //ScaleInput
                    else
                        transedScale = constraintList[i].scaleInput;

                    //非offsetならワールドスケールをポーズのスケールに変換
                    if (!constraintList[i].offset)
                        transedScale = transform.parent.InverseTransformVector(transedScale);
                }

                //offset(乗算)
                if (constraintList[i].offset)
                {
                    //補完して乗算して保持
                    if (constraintList[i].constraintWeight != 1f)
                        transedScale = Vector3.Lerp(
                            Vector3.one,
                            transedScale,
                            constraintList[i].constraintWeight);
                    copyScale =
                        new Vector3(
                            copyScale.x * transedScale.x,
                            copyScale.y * transedScale.y,
                            copyScale.z * transedScale.z);
                }
                //非offset(置き換え)
                else {
                    //補完して置き換えて保持
                    if (constraintList[i].constraintWeight != 1f)
                        transedScale = Vector3.Lerp(
                            copyScale,
                            transedScale,
                            constraintList[i].constraintWeight);
                    copyScale = transedScale;
                }
            }

            //LookAtのとき
            else if (constraintList[i].constraintType == ConstraintData.ConstraintType.LookAt &&
                constraintList[i].source &&
                constraintList[i].aimVector != Vector3.zero &&
                (constraintList[i].axisX || constraintList[i].axisY || constraintList[i].axisZ))
            {

                //保持した回転がEulerならQuaternionに
                if (copyEuler != Vector3.zero)
                {
                    copyQuat = Quaternion.Euler(copyEuler);
                    copyEuler = Vector3.zero;
                }

                //ターゲットのワールド座標を取得
                Vector3 targetVector = constraintList[i].source.position;
                //自分のローカル座標に
                if (transform.parent)
                    targetVector = transform.parent.InverseTransformPoint(targetVector);
                //保持した回転、保持した移動を換算した空間に
                targetVector = Quaternion.Inverse(restQuat * copyQuat) * (targetVector - restPosition) - copyPosition;

                //axisがXYZすべてtrueなら減衰トラック(Quaternionで取得)
                if (constraintList[i].axisX && constraintList[i].axisY && constraintList[i].axisZ)
                {
                    transedQuat = Quaternion.FromToRotation(constraintList[i].aimVector, targetVector);
                }
                //axisが1~2個trueならそれぞれ軸固定トラック(Eulerで取得)
                else if (constraintList[i].axisX || constraintList[i].axisY || constraintList[i].axisZ)
                {
                    if (constraintList[i].axisY)
                    {
                        Vector3 aimYVector = constraintList[i].aimVector;
                        aimYVector.y = 0f;
                        Vector3 targetYVector = targetVector;
                        targetYVector.y = 0f;
                        transedEuler.y =
                            Vector3.Angle(aimYVector, targetYVector)
                            * (Vector3.Cross(aimYVector, targetYVector).y < 0f ? -1f : 1f);
                    }
                    if (constraintList[i].axisX)
                    {
                        Vector3 aimXVector = constraintList[i].aimVector;
                        aimXVector.x = 0f;
                        Vector3 targetXVector = Quaternion.Inverse(Quaternion.Euler(0f, transedEuler.y, 0f)) * targetVector;
                        targetXVector.x = 0f;
                        transedEuler.x =
                            Vector3.Angle(aimXVector, targetXVector)
                            * (Vector3.Cross(aimXVector, targetXVector).x < 0f ? -1f : 1f);
                    }
                    if (constraintList[i].axisZ)
                    {
                        Vector3 aimZVector = constraintList[i].aimVector;
                        aimZVector.z = 0f;
                        Vector3 targetZVector = Quaternion.Inverse(Quaternion.Euler(transedEuler.x, transedEuler.y, 0f)) * targetVector;
                        targetZVector.z = 0f;
                        transedEuler.z =
                            Vector3.Angle(aimZVector, targetZVector)
                            * (Vector3.Cross(aimZVector, targetZVector).z < 0f ? -1f : 1f);
                    }
                    transedQuat = Quaternion.Euler(transedEuler);
                }

                //ウェイト補正
                if (constraintList[i].constraintWeight != 1f)
                    transedQuat = Quaternion.Lerp(
                        Quaternion.identity,
                        transedQuat,
                        constraintList[i].constraintWeight);

                //合成はAfterOriginal
                copyQuat = copyQuat * transedQuat;
            }
        }


        //Eulerで保持していたらQuaternionに変換して保持
        if (copyEuler != Vector3.zero) 
            copyQuat = Quaternion.Euler(copyEuler);

        //保持した移動と回転をweightで補間
        if (weight != 1f)
        {
            copyPosition = Vector3.Lerp(
                Vector3.zero,
                copyPosition,
                weight);
            copyQuat = Quaternion.Lerp(
                Quaternion.identity,
                copyQuat,
                weight);
            copyScale = Vector3.Lerp(
                Vector3.one,
                copyScale,
                weight);
        }

        //反映
        transform.localPosition = restPosition + restQuat * copyPosition;
        transform.localRotation = restQuat * copyQuat;
        transform.localScale =
            new Vector3(
                restScale.x * copyScale.x,
                restScale.y * copyScale.y,
                restScale.z * copyScale.z);
    }

    /*
    //↓再生時に2個目が消えるのでCO
    void OnValidate() 
    {
        //constraintListの長さを前回と比較して多ければ
        //多くなった分を空のConstraintDataに置き換える
        int editCount = constraintList.Count;
        if(constraintListCount < editCount)
        {
            constraintList.RemoveRange(
                constraintListCount,
                editCount - constraintListCount);
            for (int i = 0; i < editCount - constraintListCount; i++)
                constraintList.Add(new ConstraintData());
        }
        constraintListCount = editCount;
    }
    */

    //オイラー角を -180 < e <=180 に直す
    private float EulerFix(float e)
    {
        while (e > 180f) e -= 360f;
        while (e <= -180f) e += 360f;
        return e;
    }
    //Vector3(オイラー)を-180 < e <=180 に
    private Vector3 EulerFixV3(Vector3 e)
    {
        e = new Vector3(EulerFix(e.x), EulerFix(e.y), EulerFix(e.z));
        return e;
    }

    //回転を変換する
    Vector3 TransXYZ(int i, Quaternion transedQuat, Vector3 transedEuler)
    {
        if (transedQuat != Quaternion.identity) 
            transedEuler = EulerFixV3(transedQuat.eulerAngles);
        return constraintList[i].transXTo * transedEuler.x
                + constraintList[i].transYTo * transedEuler.y
                + constraintList[i].transZTo * transedEuler.z;
    }
}

[System.Serializable]
public class ConstraintData
{
    public bool isActive = true;
    public ConstraintType constraintType = ConstraintType.Position;
    [Range(0f, 1f)][Tooltip("影響量。回転は非球状補間。")]
    public float constraintWeight = 1f;
    public Transform source = null;

    public Vector3 sourceRestPosition = Vector3.zero;
    public Quaternion sourceRestQuat = Quaternion.identity;
    public Vector3 sourceRestScale = Vector3.zero;

    //Rotation, Positionのとき
    public Space space = Space.Self;

    //Rotation ローカル空間のとき
    public MixType mix = MixType.AfterOriginal;
    //Position Scale ローカル空間のとき
    public bool offset = false;

    //sourceなし
    public Vector3 positionInput = new Vector3(0f, 0f, 0f);
    public Vector3 rotationInput = new Vector3(0f, 0f, 0f);
    public Vector3 scaleInput = new Vector3(1f, 1f, 1f);

    //sourceあり
    public Vector3 transXTo = new Vector3(1f, 0f, 0f);
    public Vector3 transYTo = new Vector3(0f, 1f, 0f);
    public Vector3 transZTo = new Vector3(0f, 0f, 1f);

    //LookAtのとき
    public Vector3 aimVector = new Vector3(0f, 0f, 1f);
    public bool axisX = true;
    public bool axisY = true;
    public bool axisZ = true;

    public enum ConstraintType
    {
        Position,
        //PositionInput,
        Rotation,
        //RotationInput,
        Scale,
        LookAt,
    }
    public enum MixType
    {
        Add,
        AfterOriginal,
        Replace
    }
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ConstraintData))]
public class ConstraintDataDrawer : PropertyDrawer
{
    public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label)
    {
        Rect defaultRect, vector3Rect;
        defaultRect = vector3Rect = EditorGUI.IndentedRect( new Rect(rect){
            y = rect.y,
            height = EditorGUIUtility.singleLineHeight,
        });
        if (!EditorGUIUtility.wideMode)
            vector3Rect.height = EditorGUIUtility.singleLineHeight * 2 + 2f;


        using (new EditorGUI.PropertyScope(rect, label, property))
        {

            var isActive = property.FindPropertyRelative("isActive");
            var constraintType = property.FindPropertyRelative("constraintType");

            isActive.boolValue = EditorGUI.ToggleLeft(defaultRect,  "IsActive", isActive.boolValue);

            EditorGUI.indentLevel++;
            defaultRect.y = vector3Rect.y += defaultRect.height + 2f;

            constraintType.enumValueIndex = EditorGUI.Popup(defaultRect, "Constraint", constraintType.enumValueIndex, System.Enum.GetNames(typeof(ConstraintData.ConstraintType)));
            defaultRect.y = vector3Rect.y += defaultRect.height + 2f;

            var constraintWeight = property.FindPropertyRelative("constraintWeight");
            constraintWeight.floatValue = EditorGUI.Slider(defaultRect, "Weight", constraintWeight.floatValue, 0f, 1f);
            defaultRect.y = vector3Rect.y += defaultRect.height + 2f;

            var source = property.FindPropertyRelative("source");
            source.objectReferenceValue = EditorGUI.ObjectField(defaultRect, "Source", source.objectReferenceValue, typeof(Transform), true);
            defaultRect.y = vector3Rect.y += defaultRect.height + 2f;

            if ((ConstraintData.ConstraintType)constraintType.enumValueIndex != ConstraintData.ConstraintType.LookAt)
            {
                var space = property.FindPropertyRelative("space");
                space.enumValueIndex = EditorGUI.Popup(defaultRect, "Space", space.enumValueIndex, System.Enum.GetNames(typeof(Space)));
                defaultRect.y = vector3Rect.y += defaultRect.height + 2f;

                if ((Space)space.enumValueIndex == Space.Self &&
                    ((ConstraintData.ConstraintType)constraintType.enumValueIndex == ConstraintData.ConstraintType.Rotation))
                {
                    var mix = property.FindPropertyRelative("mix");
                    mix.enumValueIndex = EditorGUI.Popup(defaultRect, "Mix", mix.enumValueIndex, System.Enum.GetNames(typeof(ConstraintData.MixType)));
                    defaultRect.y = vector3Rect.y += defaultRect.height + 2f;
                }

                if ((ConstraintData.ConstraintType)constraintType.enumValueIndex == ConstraintData.ConstraintType.Position ||
                    (ConstraintData.ConstraintType)constraintType.enumValueIndex == ConstraintData.ConstraintType.Scale)
                {
                    var offset = property.FindPropertyRelative("offset");
                    offset.boolValue = EditorGUI.Toggle(defaultRect, "Offset", offset.boolValue);
                    defaultRect.y = vector3Rect.y += defaultRect.height + 2f;
                }

                if ((ConstraintData.ConstraintType)constraintType.enumValueIndex == ConstraintData.ConstraintType.Position && !source.objectReferenceValue)
                {
                    var positionInput = property.FindPropertyRelative("positionInput");
                    positionInput.vector3Value = EditorGUI.Vector3Field(vector3Rect, "Position Input", positionInput.vector3Value);
                    defaultRect.y = vector3Rect.y += vector3Rect.height + 2f;
                }
                else if ((ConstraintData.ConstraintType)constraintType.enumValueIndex == ConstraintData.ConstraintType.Rotation && !source.objectReferenceValue)
                {
                    var rotationInput = property.FindPropertyRelative("rotationInput");
                    rotationInput.vector3Value = EditorGUI.Vector3Field(vector3Rect, "Rotation Input", rotationInput.vector3Value);
                    defaultRect.y = vector3Rect.y += vector3Rect.height + 2f;
                }
                else if ((ConstraintData.ConstraintType)constraintType.enumValueIndex == ConstraintData.ConstraintType.Scale && !source.objectReferenceValue)
                {
                    var scaleInput = property.FindPropertyRelative("scaleInput");
                    scaleInput.vector3Value = EditorGUI.Vector3Field(vector3Rect, "Scale Input", scaleInput.vector3Value);
                    defaultRect.y = vector3Rect.y += vector3Rect.height + 2f;
                }
                else
                {
                    var transXTo = property.FindPropertyRelative("transXTo");
                    transXTo.vector3Value = EditorGUI.Vector3Field(vector3Rect, "Trans X To", transXTo.vector3Value);
                    defaultRect.y = vector3Rect.y += vector3Rect.height + 2f;

                    var transYTo = property.FindPropertyRelative("transYTo");
                    transYTo.vector3Value = EditorGUI.Vector3Field(vector3Rect, "Trans Y To", transYTo.vector3Value);
                    defaultRect.y = vector3Rect.y += vector3Rect.height + 2f;

                    var transZTo = property.FindPropertyRelative("transZTo");
                    transZTo.vector3Value = EditorGUI.Vector3Field(vector3Rect, "Trans Z To", transZTo.vector3Value);
                    defaultRect.y = vector3Rect.y += vector3Rect.height + 2f;
                }
            }
            else
            {
                var aimVector = property.FindPropertyRelative("aimVector");
                aimVector.vector3Value = EditorGUI.Vector3Field(vector3Rect, "Aim Vector", aimVector.vector3Value);
                defaultRect.y = vector3Rect.y += vector3Rect.height + 2f;

                Rect axisRect = EditorGUI.PrefixLabel(defaultRect, new GUIContent("Axis"));
                axisRect.width = 30f;
                EditorGUI.indentLevel = 0;

                var axisX = property.FindPropertyRelative("axisX");
                var axisY = property.FindPropertyRelative("axisY");
                var axisZ = property.FindPropertyRelative("axisZ");

                axisX.boolValue = EditorGUI.ToggleLeft(axisRect, "X", axisX.boolValue);
                axisRect.x += axisRect.width;

                axisY.boolValue = EditorGUI.ToggleLeft(axisRect, "Y", axisY.boolValue);
                axisRect.x += axisRect.width;

                axisZ.boolValue = EditorGUI.ToggleLeft(axisRect, "Z", axisZ.boolValue);

                defaultRect.y = vector3Rect.y += defaultRect.height + 2f;
                EditorGUI.indentLevel++;
            }
            
            EditorGUI.indentLevel--;
        }
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        float gyou = 1f;
        float gyouV3 = 1f;
        gyou += 3.3f;
        if (!EditorGUIUtility.wideMode)
            gyouV3 = 2f;

        var constraintType = property.FindPropertyRelative("constraintType");
        if ((ConstraintData.ConstraintType)constraintType.enumValueIndex != ConstraintData.ConstraintType.LookAt)
        {
            gyou += 2f;
            var space = property.FindPropertyRelative("space");
            if ((Space)space.enumValueIndex == Space.World &&
                (ConstraintData.ConstraintType)constraintType.enumValueIndex == ConstraintData.ConstraintType.Rotation)
                gyou--;

            if (property.FindPropertyRelative("source").objectReferenceValue)
                gyou += gyouV3 * 3f;
            else
                gyou += gyouV3;
        }
        else
            gyou += gyouV3 + 1f;

        return (EditorGUIUtility.singleLineHeight + 2f) * gyou - 2f;
    }
}
#endif

おひのっと ohinot 2024/02/07 15:03

【Unity】任意のオブジェクトのローカル回転をコピー、変換、合成するスクリプト

任意のオブジェクトのローカル回転をコピー、変換、合成するスクリプトです。
BlenderのConstraintのCopyRotation(ローカル空間)やTransformation(回転から回転に変換)と同じようなことをします。
利用・改変・転載ご自由に。

//任意のオブジェクトのローカル回転をコピー、変換、合成するスクリプト
//利用・改変・転載ご自由に

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

public class LocalRotationConstraint : MonoBehaviour
{
    [Tooltip("回転の影響量。クランプなし球状補間。0.5で半分の回転、2で倍の回転、0で無効、マイナスで反転。最小回転なので270度の0~1は0~270度ではなく0~-90度になる。")]
    public float weight = 1f;

    [Tooltip("ウェイト0のときの回転。")]
    public Vector3 rotationAtRest;

    [System.Serializable]
    public struct SourceData
    {
        public Transform source;
        [Tooltip("ソースごとの回転の影響量。クランプなし球状補間。")]
        public float weight;
        [Tooltip("回転軸の変換。回転軸を入れ替えたり反転したり無効にしたりする。" +
            "ただしソースのオイラーXを90度以上または-90度以下にしようとするとき、YとZを無効にしたり減衰させていると、望まない回転をすることがある。")]
        public Vector3 transXTo;
        [Tooltip("回転軸の変換。")]
        public Vector3 transYTo;
        [Tooltip("回転軸の変換。")]
        public Vector3 transZTo;

        public SourceData(Transform t, float w, Vector3 x, Vector3 y, Vector3 z) 
        {
            source = t;
            weight = w;
            transXTo = x;
            transYTo = y;
            transZTo = z;
        }
    }

    [Tooltip("ソースリスト。上から順に適用。")]
    [SerializeField]
    public List<SourceData> sourceList = new List<SourceData> { 
        new SourceData(
            null, 
            1f, 
            new Vector3(1,0,0), 
            new Vector3(0,1,0), 
            new Vector3(0,0,1) 
        ) 
    };

    void Update()
    {
        //静止時の回転
        Quaternion rotationAtRestQ = Quaternion.Euler(rotationAtRest);
        Quaternion copyQuat = rotationAtRestQ;
        //ソースごとに
        for (int i = 0; i < sourceList.Count; i++) 
        {
            if (sourceList[i].source)
            {
                //ソースをオイラー角にして回転軸を変換
                Vector3 transedV3 = EulerFixV3(sourceList[i].source.localEulerAngles);

                transedV3 =
                    sourceList[i].transXTo * transedV3.x
                    + sourceList[i].transYTo * transedV3.y
                    + sourceList[i].transZTo * transedV3.z;

                //ソースのweightで補間して合成
                copyQuat = Quaternion.SlerpUnclamped(
                    Quaternion.identity,
                    Quaternion.Euler(transedV3),
                    sourceList[i].weight
                ) * copyQuat;
            }
        }

        //transformにweightで補間して反映
        transform.localRotation = Quaternion.SlerpUnclamped(
            rotationAtRestQ, 
            copyQuat, 
            weight);
    }

    //オイラー角を -180 < e <=180 に直す
    private float EulerFix(float e)
    {
        if (e > 180f) e -= 360f;
        if (e <= -180f) e += 360f;
        return e;
    }
    //Vector3(オイラー)を-180 < e <=180 に
    private Vector3 EulerFixV3(Vector3 e) {
        e = new Vector3(EulerFix(e.x), EulerFix(e.y), EulerFix(e.z) );
        return e;
    }
}

おひのっと ohinot 2024/01/06 22:02

【Unity】人間の性的身体を物理演算で表現するデモ


人間の性的身体を物理演算で表現するデモです。

Particle ベースの Softbody シミュレーションでは、Particle の密度に対して充分小さいコリジョンを接触させたとき、意図しない突き抜けが発生しやすいらしいことが分かりました。
このデモでは、体の大まかな形状と動きを Unity標準Collider と Joint で表現し、皮膚面上の細かな動きのみを Softbody で表現することで破綻を減らすようにしました。

Asset: Obi Softbody / Obi Fluid
モデルは自作

« 1 2

記事のタグから探す

月別アーカイブ

限定特典から探す

記事を検索