ChatGPTにUnityシーン内のオブジェクトの移動を記録するスクリプトを書かせてみた

はじめに

Twiterで、以下のようなツイートを見かけた。

これが出来るのなら、(ある程度ユーザがUnityを分かっている前提で、)VR実験に必要な種々のスクリプトを実装させられるのでは、と考えた。

 

要件

そこで、この記事ではChatGPTに「シーン内のオブジェクトの移動記録を記録し、csvファイルに保存する」スクリプトを作らせてみる。

 

とりあえず書かせてみる。

 

 

using UnityEngine;
using System.IO;
using System.Text;

public class SaveTransformData : MonoBehaviour
{
    // 保存する対象のゲームオブジェクト
    [SerializeField] private GameObject targetObject;

    // ファイル名
    [SerializeField] private string fileName = "TransformData.csv";

    // コンマ区切りの文字列
    private readonly string DELIMITER = ",";

    // 一行目のパラメータ名
    private readonly string PARAMETERS = { "Name", "Position.x", "Position.y", "Position.z", "Rotation.x", "Rotation.y", "Rotation.z", "Rotation.w" };

    // ファイルに書き込む文字列
    private StringBuilder csvContent;

    private void Start()
    {
        // 一行目のパラメータ名を追加
        csvContent = new StringBuilder();
        for (int i = 0; i < PARAMETERS.Length; i++)
        {
            csvContent.Append(PARAMETERS[i]);
            if (i < PARAMETERS.Length - 1)
            {
                csvContent.Append(DELIMITER);
            }
        }
        csvContent.AppendLine();
    }

    private void Update()
    {
        if (targetObject == null)
        {
            return;
        }

        // オブジェクトの名前、座標、回転を取得
        string name = targetObject.name;
        Vector3 position = targetObject.transform.position;
        Quaternion rotation = targetObject.transform.rotation;

        // 文字列に追加
        csvContent.Append(name);
        csvContent.Append(DELIMITER);
        csvContent.Append(position.x);
        csvContent.Append(DELIMITER);
        csvContent.Append(position.y);
        csvContent.Append(DELIMITER);
        csvContent.Append(position.z);
        csvContent.Append(DELIMITER);
        csvContent.Append(rotation.x);
        csvContent.Append(DELIMITER);
        csvContent.Append(rotation.y);
        csvContent.Append(DELIMITER);
        csvContent.Append(rotation.z);
        csvContent.Append(DELIMITER);
        csvContent.Append(rotation.w);
        csvContent.AppendLine();
    }

    private void OnApplicationQuit()
    {
        // ファイルに書き込み
        File.WriteAllText(fileName, csvContent.ToString());
    }
}

 

この時点で、それなりのものが出来上がった。出来上がったスクリプトをアタッチしてみると、エラーもなくcsvに出力され、きちんと保存された。

 

このスクリプトには以下のような不満点があった。

・一つのオブジェクトしかtransformを保存できない

・保存場所をAssets/Log 以下にしたい

・rotationがquaternion形式になっている

・csvContentが結構重くなりそう。 File.AppendAllTextを用い、毎フレーム保存してほしい。

 

これらを直させてみる。

 

 

何回かに指示を分けて、対話的にスクリプトを修正させた結果が以下である。

完成したスクリプト

using UnityEngine;
using System.IO;
using System.Text;
using System;

public class SaveMultipleTransformData : MonoBehaviour
{
    // 保存する対象のゲームオブジェクト
    [SerializeField] private GameObject targetObjects;
    // ファイル名
    [SerializeField] private string fileName = "TransformData.csv";

    // 保存先のフォルダパス
    [SerializeField] private string folderPath = "/Assets/LogTest";

    // コンマ区切りの文字列
    private readonly string DELIMITER = ",";

    // ファイルに書き込む文字列
    private StringBuilder csvContent;

    private string filePath;

    private void Start()
    {
        if (targetObjects.Length == 0)
        {
            return;
        }
        filePath = Environment.CurrentDirectory + folderPath + "/" + fileName;

        // フォルダが存在しなければ作成
        if (!Directory.Exists(Environment.CurrentDirectory + folderPath))
        {
            Directory.CreateDirectory(Environment.CurrentDirectory + folderPath);
        }

        // 一行目のパラメータ名を追加
        csvContent = new StringBuilder();
        for (int i = 0; i < targetObjects.Length; i++)
        {
           
            string objectName = targetObjects[i].name;
            csvContent.Append(objectName + "_Position_x" + DELIMITER);
            csvContent.Append(objectName + "_Position_y" + DELIMITER);
            csvContent.Append(objectName + "_Position_z" + DELIMITER);
            csvContent.Append(objectName + "_Rotation_x" + DELIMITER);
            csvContent.Append(objectName + "_Rotation_y" + DELIMITER);
            csvContent.Append(objectName + "_Rotation_z" + DELIMITER);
        }
        csvContent.AppendLine();
    }

    private void Update()
    {
        for (int i = 0; i < targetObjects.Length; i++)
        {
            if (targetObjects[i] == null)
            {
                continue;
            }

            // オブジェクトの座標、回転を取得
            Vector3 position = targetObjects[i].transform.position;
            Vector3 rotation = targetObjects[i].transform.eulerAngles;

            // 文字列に追加
            csvContent.Append(position.x + DELIMITER);
            csvContent.Append(position.y + DELIMITER);
            csvContent.Append(position.z + DELIMITER
            );
            csvContent.Append(rotation.x + DELIMITER);
            csvContent.Append(rotation.y + DELIMITER);
            csvContent.Append(rotation.z + DELIMITER);
        }
        csvContent.AppendLine();

        File.AppendAllText(filePath, csvContent.ToString());
        csvContent.Clear();

    }
}
このスクリプトで、良い感じに動いた。

ありがてえ........
 

感想

まだタイムスタンプなどの細かい機能は付けていないが、一先ずChatGPTさんが実用に耐えることを確認できた。
また、上のコードのDELIMITER = ",";とした部分や、StringBuilderを用いた実装などは自分には考えつかない部分だった。学びが多い。
これからは、ChatGPTを活用することで実験Utilityに関する実装時間を多く削減できそうだ。

quaternionによるローカル座標回転を考えた【Unity】【Quaternion】【Local Coordinate】【Rotation】

クォータニオンでのローカル軸回転に苦しめられたので書き残しておきます。

 

結論

ある物体の回転角がクォータニオンpであり、それをワールド座標でクォータニオンqだけ回転させる場合は

q⊗p=(qwpx−qzpy+qypz+qxpw,qzpx+qwpy−qxpz+qypw,−qypx+qxpy+qwpz+qzpw,−qxpx−qypy−qzpz+qwpw)

が変換後の回転角となる。

一方、同じ物体(回転角p)を、ローカル座標でクォータニオンqだけ回転させる場合は

p⊗q

が変換後の回転角となる。

これは、ワールド座標でq回転している物体にpを作用させたと理解できる。すなわち、回転角pをローカル座標でq回転させた結果の回転角は、回転角qをワールド座標でp回転させた結果の回転角と等しくなる。


初めに

ある物体の回転(姿勢)はquaternionを用いて一意に記述することができます。

q = (q_x,q_y,q_z,q_w)  (ただし、q_x^2 + q_y^2 + q_z^2 = 1)

剛体の回転自由度は三自由度であり、Unityのinspector上では直感的に分かりやすいオイラー角を用いて表されています。しかしながら、オイラー角は計算機にとって計算がしにくく、またジンバルロックなどの問題もあるため、実際に計算される時にはquaternionが用いられています。

 

ワールド座標での回転

ある物体の姿勢がクォータニオンpであり、それを(ワールド座標で)クォータニオンqだけ回転させるとき、

q⊗p=(qwpx−qzpy+qypz+qxpw,qzpx+qwpy−qxpz+qypw,−qypx+qxpy+qwpz+qzpw,−qxpx−qypy−qzpz+qwpw)

が回転後の物体の姿勢を表すクォータニオンになります。


例えばワールド座標x軸回りの回転は

r = (λ_x sinΘ/2, λ_y sinΘ/2,λ_z sinΘ/2,cosΘ/2 ) 
   = (1,0,0,0)

となるので、

p = (p_x, p_y, p_z, p_w)をx軸回りに180度回転させると

r⊗p = (p_w, -p_z, p_y, -p_x)

となります。

 

Unityにおいては、簡単に

gameObject.transform.Rotate(180,0,0,Space.World);

と記述できます。

 

ローカル座標での回転

ワールド座標における回転は以上の通りですが、ローカル座標で回転させるにはどうすればよいでしょうか。

 

ローカルx軸回り180度回転は、Unityにおいては、簡単に

gameObject.transform.Rotate(180,0,0,Space.Local);

と記述できます。

 

ところで、pをローカル座標でq回転させるのは、qをワールド座標でp回転させるのと等しくなります。

よって、クォータニオンpをローカル座標でクォータニオンq回転させたときは、

p⊗q

が回転後の物体の姿勢を表すクォータニオンになります。

 

例えばローカル座標x軸回りの回転は

r = (λ_x sinΘ/2, λ_y sinΘ/2,λ_z sinΘ/2,cosΘ/2 ) 
   = (1,0,0,0)

となるので、

p = (p_x, p_y, p_z, p_w)をx軸回りに180度回転させると

p⊗r = (p_w, p_z, -p_y, -p_x)

となります。

余談:Quaternionの計算をUnity上で態々書かなくても、transform.Rotateで良くないか?この記事のモチベは?

ラッキングデータなど、外部ソフトから回転角をUnityに持ってきて、Unityの座標系・モデルの座標系に変換し、モデルを動かすことがあります。

自分の場合は、外部ソフトから持ってきた回転角が右手座標系であったため、それをUnityの左手座標系にして、更にボーンの初期回転角に依る角度ずれを補正し、Unity側モデルの座標系に合わせる必要が生じました。一先ず書いてみると、以下のようになりました。

r = (外部から持ってきたrotation);

r = new Quaternion(r.z,-r.x,r.y,-r.w); //座標変換

this.transform.rotation = r;

this.transform.Rotate(180, 0, 0, Space.Self); //180度ずれを補正

 

この2行目と4行目の処理を(計算速度の観点から)一行に減らしたかったため、transform.Rotate(180,0,0 Space.self)をクォータニオンの計算で何とか表せないかと考えたためです。

 

上で書いたことから、transform.Rotate(180, 0, 0, Space.Self)はr ⊗ (1,0,0,0)と考えられるので、コードは以下のようになりました。

 

r = (外部から持ってきたrotation);

r = new Quaternion(-r.w,r.y,r.x,-r.z); //座標変換

this.transform.rotation = r;

 

シンプルな実装ではありますが、自分は「クォータニオンpをローカル座標でクォータニオンq回転させた結果はクォータニオンqをワールド座標でクォータニオンp回転させた結果(p⊗q)と等しくなる」ということに中々気がつきませんでした。

 

pを(ローカル座標で)q回転させるためのクォータニオンを得ようと考え、q'⊗p = p⊗qとなるq'を無駄に考えていました。残念。

 

クォータニオンによるローカル座標回転はググっても出てこなかったので、一応書き物として残しておきます。この記事を見つけた人の助けとなりますよう。

 

ちなみに、回転を反転させる方法は以下の通りです。

全方向反転:

(q_x,q_y,q_z,-q_w)

x軸方向のみ反転:

(q_x,-q_y,-q_z,q_w)

y軸方向のみ反転:

(-q_x,q_y,-q_z,q_w)

z軸方向のみ反転:

(-q_x,-q_y,q_z,q_w)

 

 

SteamVRの視点キャリブレーション(Recenter)をキーボードから呼び出す

 VR技術のデモをやっているとき、体験者の視点をVRシーンの中央へとリセットしたいことがあります。普通ならSteamVRのダッシュボードを開き、右下のアイコンから立ち位置のリセットを行わなければならず、体験者がVR慣れしていないときには以下のような対応が必要でした。

・体験者に視点リセット方法を教える

・SteamVRの『VRビューを表示』で表示したVRビューを見つつ、展示者がコントローラを操作して視点リセットする

 

 先日、この視点リセットをUnity内のスクリプトから呼び出す方法を知ったので書き残しておきます。

 

 ある程度前のUnity(2018以前?)なら、using UnityEngine.XRしてInputTracking.Recenter()を呼べばokです。

 

 新しい方のUnity(2020以降?)なら、以下のスクリプトのようにすればokです。これでSpaceキーを押したら視点がリセットされます。ほかのスクリプトとは独立してるので、Recenter.csを作成し、以下をコピペし、適当なオブジェクトに貼り付ければ動きます。

 

using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.Management;

public class ReCenter : MonoBehaviour
{
    XRInputSubsystem xrInput;
    public void Start()
    {
        var xrSettings = XRGeneralSettings.Instance;
        xrInput = xrSettings.Manager.activeLoader.GetLoadedSubsystem<XRInputSubsystem>();
    }

    void Update(){
        if(Input.GetKeyDown(KeyCode.Space)){
            xrInput.TryRecenter();
            Debug.Log("Recenter");
        }
    }
}
 
 

 検索用キーワード:SteamVR、視点、リセット、トラッキング、Recenter、プレイスペース、センタリング、中央に配置、プレイエリアのリセット、ポジションリセンター

追記: Oculus Quest2 (Oculus Integreation)の場合
以下を参照すると上手く行った。
Recenter()のコード部分だけでも動く気がするが...?

Re: How to disable Quest recentering in Unity XR 2... - Meta Community Forums - 822781

using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.Management;

public class ReCenterOculus : MonoBehaviour
{
    public Transform CameraOffset; // set to parent of main Camera

    Vector3[] mBoundary;

    public void Start()
    {
        OVRManager.display.RecenteredPose += OculusRecenter;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Delete))
        {
            Recenter();
            Debug.Log("Recenter");
        }
    }

    void RotateXZ(Vector3 a, float rot, out Vector3 b)
    {
        // rot is clockwise radians
        var sin = Mathf.Sin(rot);
        var cos = Mathf.Cos(rot);
        b = new Vector3(a.x * cos + a.z * sin, a.y, -a.x * sin + a.z * cos);
    }

    // Instead of recentering, don't recenter!
    void OculusRecenter()
    {
        var points = OVRManager.boundary.GetGeometry(OVRBoundary.BoundaryType.OuterBoundary);

        if (mBoundary != null && points.Length == mBoundary.Length)
        {
            var deltaPos0 = mBoundary[0] - points[0];
            var newPointMid = points[points.Length / 2] + deltaPos0;
            var vec0ToOldMid = mBoundary[points.Length / 2] - mBoundary[0];
            var vec0ToNewMid = newPointMid - mBoundary[0];
            var angOldMid = Mathf.Atan2(vec0ToOldMid.z, vec0ToOldMid.x);
            var angNewMid = Mathf.Atan2(vec0ToNewMid.z, vec0ToNewMid.x);
            var deltaRot = angOldMid - angNewMid;

            while (deltaRot > Mathf.PI)
                deltaRot -= 2 * Mathf.PI;
            while (deltaRot < -Mathf.PI)
                deltaRot += 2 * Mathf.PI;

            var reverse0 = -points[0];
            RotateXZ(reverse0, -deltaRot, out reverse0);
            reverse0 += points[0];
            var pos = CameraOffset.TransformPoint(deltaPos0 + reverse0);
            pos = CameraOffset.parent.InverseTransformPoint(pos);
            CameraOffset.localPosition = pos;

            var angs = CameraOffset.localEulerAngles;
            angs.y -= deltaRot * Mathf.Rad2Deg;
            CameraOffset.localEulerAngles = angs;
        }

        mBoundary = points;
    }

    public void Recenter()
    {
        mBoundary = OVRManager.boundary.GetGeometry(OVRBoundary.BoundaryType.OuterBoundary);

        Transform cam = Camera.main.transform;
        float yawOffset = -cam.localEulerAngles.y;
        float ang = -yawOffset * Mathf.Deg2Rad;
        float sinAng = Mathf.Sin(ang);
        float cosAng = Mathf.Cos(ang);
        float xOffset = cam.localPosition.z * sinAng - cam.localPosition.x * cosAng;
        float zOffset = -cam.localPosition.z * cosAng - cam.localPosition.x * sinAng;

        CameraOffset.localPosition = new Vector3(xOffset, 0, zOffset);
        CameraOffset.localEulerAngles = new Vector3(0, yawOffset, 0);
    }
}

VRシーン内で手を用いてスライダを操作する[unity][steamVR]

やりたいこと

・SteamVRシーン中で手を用いてスライダの値を操作(キャリブレーションに用いる)

youtu.be

 

背景

VRシーン中でゲームプレイヤーに音響などの設定を操作させたかった。よくあるレーザーポインタ方式はほかのシーンと一貫していなかったので使いたくなかった。SteamVR中でスライダをつかんで操作する実装が載っているサイトはなく、寧ろいくつか質問トピックが回答なしの状態で放置されていたため、書くことにした。

 

実装

指先に付けた当たり判定がスライダ上の当たり判定に当たったときにスライダの両端の座標を求め、指でスライダを握っているときには指先の座標のスライダ上での位置を求めてslider.valueに代入する。指を開いたらスライダを離す。

 

PointerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;
using UnityEngine;
using UnityEngine.UI;
public class PointerController : MonoBehaviour
{
    StimulusPoint_config stimulusPoint;
    Slider slider;

    int slider_Max;
    int slider_min;
    bool preGrabValue = false;

    Vector3 slider_left;
    Vector3 slider_right;

    void Start()
    {
        //指先位置につけたgameobjectにつけたスクリプト 握っている状態ではstimulusPoint.Grabbingがtrueになる
        stimulusPoint = gameObject.GetComponent<StimulusPoint_config>();
    }
   
    void OnTriggerStay(Collider col)
    {
        //スライダ上にsphereコライダを置いておく
        if (col.gameObject.CompareTag("Slider") && stimulusPoint.Grabbing && slider == null)
        {
            Debug.Log("Grab");
            //スライダを得る
            slider = col.gameObject.GetComponentInParent<Slider>();
            Debug.Log(slider);
            slider_Max = (int)slider.maxValue;
            slider_min = (int)slider.minValue;
            Vector3[] v = new Vector3[4];
            slider.gameObject.GetComponent<RectTransform>().GetWorldCorners(v);
            //スライダの左端、右端のworld座標を得る
            slider_left = (v[0] + v[1])/2;
            slider_right = (v[2] + v[3])/2;
        }
    }

    void OnTriggerExit(Collider col){
        //slider = null;
    }
   
    void Update()
    {
        //指を開いたときにスライダを離す
        if (slider != null)
        {
            //指を開いた瞬間にのみ以下を実行
            if(preGrabValue != stimulusPoint.Grabbing && !stimulusPoint.Grabbing){
                slider = null;
                Debug.Log("slider ungrab");
            }
        }

        if (slider != null)
        {
            //自分の握っている指の先端位置がスライダ上のどの位置にあるかを割合で出す
            float ratio = Vector3.Dot(stimulusPoint.transform.position - slider_left, slider_right - slider_left) / (float)Math.Pow(Vector3.Magnitude(slider_right - slider_left), 2);
 
            Debug.Log(ratio);
            //上の割合を用いてスライダのvalueを定める 今回はスライダは整数値なので適当に丸める
       if (slider.wholeNumbers == true)
            {
                slider.value = slider_min + Mathf.Round((slider_Max - slider_min) * ratio);
            }else{
                slider.value = slider_min + (slider_Max - slider_min) * ratio;
            }
        }

        //指の開閉を記録
        preGrabValue = stimulusPoint.Grabbing;
    }
}
StimulusPoint_config.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class StimulusPoint_config : MonoBehaviour
{
    Material mat;
    public bool Grabbing;
    public bool Drawing;
    [SerializeField] GameObject IndexTip;
    [SerializeField] GameObject ThumbTip;
    [SerializeField] float Threshold = 0.04f;

    void Start()
    {
        mat = gameObject.GetComponent<Renderer>().material;
    }

    // Update is called once per frame
    void Update()
    {
       // 人差し指と親指の距離で開閉を判断
        if ((IndexTip.transform.position - ThumbTip.transform.position).magnitude < Threshold)
        {
            //Debug.Log((IndexTip.transform.position - ThumbTip.transform.position).magnitude);
            //Debug.Log("Grab");
            Grabbing = true;
            mat.color = Color.red;
        }else{
             Grabbing = false;
             mat.color = Color.white;
        }
    }
}

 

スライダの当たり判定の為にsphereをここにおく

お気持ち

レーザーポインタでUIを操作するのは体験の価値を損ねうるため気軽に用いるべきではないかもしれない(距離とかを考えずに済むため便利だけど)

VRシーン内で手を用いてスライダを操作する[unity][steamVR]

やりたいこと

・SteamVRシーン中で手を用いてスライダの値を操作(キャリブレーションに用いる)

youtu.be

 

背景

VRシーン中でゲームプレイヤーに音響などの設定を操作させたかった。よくあるレーザーポインタ方式はほかのシーンと一貫していなかったので使いたくなかった。SteamVR中でスライダをつかんで操作する実装が載っているサイトはなく、寧ろいくつか質問トピックが回答なしの状態で放置されていたため、書くことにした。

 

実装

指先に付けた当たり判定がスライダ上の当たり判定に当たったときにスライダの両端の座標を求め、指でスライダを握っているときには指先の座標のスライダ上での位置を求めてslider.valueに代入する。指を開いたらスライダを離す。

 

PointerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;
using UnityEngine;
using UnityEngine.UI;
public class PointerController : MonoBehaviour
{
    StimulusPoint_config stimulusPoint;
    Slider slider;

    int slider_Max;
    int slider_min;
    bool preGrabValue = false;

    Vector3 slider_left;
    Vector3 slider_right;

    void Start()
    {
        //指先位置につけたgameobjectにつけたスクリプト 握っている状態ではstimulusPoint.Grabbingがtrueになる
        stimulusPoint = gameObject.GetComponent<StimulusPoint_config>();
    }
   
    void OnTriggerStay(Collider col)
    {
        //スライダ上にsphereコライダを置いておく
        if (col.gameObject.CompareTag("Slider") && stimulusPoint.Grabbing && slider == null)
        {
            Debug.Log("Grab");
            //スライダを得る
            slider = col.gameObject.GetComponentInParent<Slider>();
            Debug.Log(slider);
            slider_Max = (int)slider.maxValue;
            slider_min = (int)slider.minValue;
            Vector3[] v = new Vector3[4];
            slider.gameObject.GetComponent<RectTransform>().GetWorldCorners(v);
            //スライダの左端、右端のworld座標を得る
            slider_left = (v[0] + v[1])/2;
            slider_right = (v[2] + v[3])/2;
        }
    }

    void OnTriggerExit(Collider col){
        //slider = null;
    }
   
    void Update()
    {
        //指を開いたときにスライダを離す
        if (slider != null)
        {
            //指を開いた瞬間にのみ以下を実行
            if(preGrabValue != stimulusPoint.Grabbing && !stimulusPoint.Grabbing){
                slider = null;
                Debug.Log("slider ungrab");
            }
        }

        if (slider != null)
        {
            //自分の握っている指の先端位置がスライダ上のどの位置にあるかを割合で出す
            float ratio = (stimulusPoint.transform.position.x - slider_left.x)/(slider_right.x - slider_left.x);
            Debug.Log(ratio);
            //上の割合を用いてスライダのvalueを定める 今回はスライダは整数値なので適当に丸める
            slider.value = slider_min + Mathf.Round( (slider_Max - slider_min) * ratio);
        }

        //指の開閉を記録
        preGrabValue = stimulusPoint.Grabbing;
    }
}
StimulusPoint_config.cs
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class StimulusPoint_config : MonoBehaviour
{
    Material mat;
    public bool Grabbing;
    public bool Drawing;
    [SerializeField] GameObject IndexTip;
    [SerializeField] GameObject ThumbTip;
    [SerializeField] float Threshold = 0.04f;

    void Start()
    {
        mat = gameObject.GetComponent<Renderer>().material;
    }

    // Update is called once per frame
    void Update()
    {
       // 人差し指と親指の距離で開閉を判断
        if ((IndexTip.transform.position - ThumbTip.transform.position).magnitude < Threshold)
        {
            //Debug.Log((IndexTip.transform.position - ThumbTip.transform.position).magnitude);
            //Debug.Log("Grab");
            Grabbing = true;
            mat.color = Color.red;
        }else{
             Grabbing = false;
             mat.color = Color.white;
        }
    }
}

 

スライダの当たり判定の為にsphereをここにおく

 

Mechanical Brain Hackingメイキング【IVRC2022 Laval Virtual🏆】

まえがき

ラボ同期と3人で作ったVR作品、Mechanical Brain Hackingが歴史あるVRコンテストIVRC2022にてSeed Stageを突破し、Leap Stageに進出し、Laval Virtual Prizeを頂きました。

 

この記事は弊作品のメイキング記事です。

 

デモ会場にて、お絵描きAI等を用いて作品制作を行ったことをスタッフの方々に話した際に、メイキング記事を書いたらいいよと打診されましたので、こうして書き記しています。

 

 

作品イメージ

 

だれむけか

  • VR作品作成初心者
  • お絵描きAIを作品制作に取り入れたい人

要約

  • AIにイラストを描かせて3Dモデリングしたら、見た目が良くなる

自分のセンスでモデリングしたロボット

AIに絵をかかせてモデリングした後頭部手術マシン

手術マシンのデザインに合わせモデリングされた部屋

本作品の説明

生体の脳は侵しがたく、活動を観察し難く、それゆえに神秘性を持つ。では、機械の脳ならば?本企画では、手足の殆どが動かない壊れかけのロボットとなり、自らの脳を開いて脳内回路を修理し、身体システムをリアルタイムに修復・改変する体験を提供する。ロボット脳内の「意識モジュール」から四肢へ伸びる回路を繋ぎ直すことにより、四肢の操作を獲得する。また、回路を繋ぎ間違えた時には、現実と異なった形で四肢が動作するようになる。脳を侵される感覚を触覚刺激により、身体操作を獲得する感覚を電気刺激により提示する。自らの脳・身体システムの改変を通し、ユーザは機械身体を持つ意識体としての価値観に触れる。

 

ロボットになって自分の脳内を手術マシンを用いて弄りまわして身体システムを改変します。

 

脳内-正しく繋がっている状態

初期状態 右腕のみが動く

脳内-間違って繋がっている状態
現実の右腕の動きに対応し、アバタの右腕、左腕が動く
現実の左腕の動きに対応し、アバタの右足が動く
現実の右足の動きに対応し、アバタの左足が動く

以下、作品制作の流れになります。

1.アイデア出し

5/31がIVRC2022の企画応募締め切りであり、それに向けてチームで話し合いアイデアを出しました。

 

初めに、過去のIVRCの受賞作品を検討した結果、以下の要素がウケそうであることが分かりました。

  • スポーツ系
  • 海外受けするもの
  • 啓蒙系
  • マルチモーダルな入出力、フィードバック
  • 見た目の面白さ

これらの要素を念頭に置き、ブレストを行いました。

  • VRでロボットになって、自分自身の脳内回路を繋ぐ→生誕する 
  • 足からジェットVR(アイアンマン、鉄腕アトム) 
  • テーブルクロス引き
  • 酔っ払い体験 飲酒運転 

以上の4つのアイデアが人気でしたが、実現可能性や面白さ、審査員ウケ、新規性を考え、投票により一番上のアイデアが選ばれました。

デモ会場でも、サイバネっぽさと一見した発想の狂気性、フィードバックシステムが良くウケていましたので、このアイデアにして正解だったと思います。

 

2.作品の方向性決め

幸いにして自分が出したアイデアが選ばれたので、自分を中心にシステム概観を考えていきました。

今回のアイデアの着想は、ロボットになり自分の脳内を覗き込むという点をテッド・チャンの「息吹」から、身体システムの編集という点を研究テーマの検討から得ていました。それゆえ、自分の脳内の改造システム、後頭部手術マシンのイメージは早々に固まりました。

 

初めは脳内の切れている回路を正常に直し、四肢を取り戻すことだけを検討していましたが、現実の身体から異なるアバタ肉体への動作リマッピングについても最近考えていたため、リアルタイムに脳内回路を繋ぎ変えて身体システムを狂わせることを思いつきました。

手足の繋ぎ変え動作の着想元 NARUTOより

VRコンテストの過去の受賞作品を見ていると、マルチモーダルなフィードバックシステムが多く使われていました。そこで、私たちもいくつかのフィードバックシステムを作ることにしました。

  1. 手術マシンの動作に合わせた後頭部への振動分布の提示
  2. 手足の操作を獲得した時の電気刺激

2は偶然チームの一人が筋電気刺激を行っていたことから着想しましたが、ハッタリが効いており、面白いシステムになりました。

 

現地デモ時にこれに似ていると言われて「なるほど~~」と思った 鋼の錬金術師より

 

3.システム制作

以下、VRシステムのメイキングになります。

今回のシステムは、大別して

  1. Unity・HMD
  2. Arduino・フィードバック系

からなります。1及びシステム統合については自分が、2についてはチームメイトが製作しました。

UnityとArduino間の通信には、Uduinoというアセットを用いました。

 

3.1 Unity・HMD

VRシーンの作成にはUnity + SteamVRを用いました。モデリングにはblender + substance painterを用いました。

3.1.1 モデリング

初めにロボットアバタをモデリングしました。かっこいい見た目にするのは大変なため、一先ず最低限のチープな見た目で作成しました。【映画】ロボッツ 予告 - YouTube等を参考にしました。VRc向けのアバターモデリングした経験が活きました。

 

続いてはロボットが自分の脳内を手術する部屋、手術マシンのモデリングに取り組みました。

ちょうどこの時期にお絵描きAIブームが来ていたので、何となくDALL-Eにコンセプトアートを描かせてみました。

DALL-Eにコンセプトアートを描いてもらった

この手術マシンの見た目が恰好よかったので、この絵をベースにモデリングしていくことにしました。また、以降のワークフローでもAIを活用することにしました。

手術マシンからモデリングをはじめ、それっぽい部屋を作成

続いて脳内の機器類をモデリングしていきました。まずはStableDiffusionに絵を描かせます。

 

anatomical chart of robot's head, messy gears and cables, cyberpank, dark, intricate, unreal engine, dark soul, --beta

このような絵を10枚くらい描かせ、モデリングできそうな構造を探し、バッシュ的に脳内を組んでいきます。ケーブル類はGeoCable, Cablator等のアドオンを使って作成しました。

 

脳内構造 側頭部の円形のパーツがカッケェ 椎骨っぽいのも良い
リアルロボットを作る先輩から「スカスカだね」と指摘されましたorz

今回のシステムで重要な脳内プラグ類は最後に作成
3.1.2 アルゴリズム設計

Unity上でSteamVRのサンプルシーンを開き、要素を追加していきました。

  • 手術マシンをテレイグジスタンス的に動かすシステム
  • プラグ間を光線で繋ぐシステム
  • 繋がった回路に合わせアバタの手足を動かす、動作リマッピングシステム
  • Arduinoへの動作命令システム

これらを適当に作成しました。

脳内回路を繋ぐ
3.1.3 演出設計

以上の「最低限のシステム」が完成したのが、コンテストにデモ動画を提出しなければいけない8/31日でした。動画を何とか提出し、以降の二日間では演出を強化していきました。

  • 頭部のカバーを開くシステム
  • ライティング
  • マテリアルの調整
  • ブルームの作成
  • 音響の作成
  • 椅子の作成
  • モデリングの調整
  • 脳内コアにエフェクトを付ける
  • 操作しやすくするための当り判定の調整
  • プラグ類へのアウトラインの描画

等を二日間で改良しました。

 

before

After

ライティングはめっちゃ大事。

 

3.2 Arduino・フィードバック系

3.2.1 電気刺激系

Uduinoを通じて電気刺激システムを動かしました。リレー回路を使ったりUnityからPythonで立てたローカルホストにhttpリクエストを送って通信したり、色々大変でしたが割愛します。

安全対策のために、電気刺激系とその他で電源を分けるようにしました。

3.2.2 後頭部振動子

Arduino Megaの20ピンに直接安い振動子を20個つなぎました。体験者の頭からコードが伸びている様子がサイバネっぽくてクールでした。

サイバネ俺

4.デモ準備

デモ前日にパッキングを行いました。Vive Pro2、ベースステーション、三脚等のVR系でスーツケース一つ、フィードバック系でランドセル一つ分ほどの荷物になりました。すごくコンパクト!海外にも持っていき易そうです。

 

5.追記 LeapStageにむけて

無事Leap Stageに進み、Laval Virtual賞、VR学会賞、ICAT賞、XR Kaigi出展などの賞を受賞しました。

 

Leap Stageに向けた改善点は以下の通りでした。

  • EMS系
    • 配線も予備を用意して、簡単に取り替えられるようにする
    • Arduinoと配線の接合部を補強
    • 足のパッドを小さくし、足に刺激がちゃんと流れるようにする
    • 電流のキャリブレーションシステムを作成する
    • 予備の配線(5本目)を用意
    • 同意書
      • 強い光がダメな人
      • 15歳以下の人?
      • 15分の拘束時間があるのがダメな人
      • 怖い作品がダメな人
      • 電気刺激ダメな人
    • 振動子
      • 切れている部分を繋ぐ
      • 切れにくい形に補強する
      • 洗濯して汗を取る
      • ブレットボードを辞めるか、周囲から見えなくする
      • システム全体として見た目を良くする
      • 頭部内部に感じられる刺激
  • VR
    • 回路をランダムに繋ぐボタンの準備
      • 見た目と音響の調整、肢で手を動かす場合でも起動できるように位置の調整
    • 尻の高さのキャリブレーションシーンの準備
    • シーンの遷移
    • 鏡を用意して自分が自分であることを確かめる
    • 日本語で説明せずとも動かせるストーリー
      • EASY ENGLISH
    • 最初に振り向く必要を無くす
      • 横か何処かに鏡を準備?
    • 初手で右手が切れないようにする
    • 操作性のアップ
    • 追加の手足パーツの獲得
      • 四肢を繋ぎ終わる→起動マシンの横の腕に触れられるようになる→持てる→肩にとりつけると動く
    • 手術マシンの起動を行う
    • 右手-左手での指のリマッピング、足指のマッピング
    • 説明動画
    • ライティング
    • VRシーンに色々画像を貼る
    • ストーリーがない
  • 全体
    • 暗幕→ラボにあれば借りる
    • ハードをサイバネにする LEDぴかぴか
    • ポスターの準備
    • 座布団
    • 動かなくて高さを調節できる椅子
    • ディスプレイの準備
    • 延長コード
    • スカートの女性のためにブランケットを準備
    • 頭汗パッド
    • 作品が提示されいる空間をつくる

 

以上を直したLeapでのデモの様子はこのようでした。

youtu.be

 

6.ウケた点の考察

自分の作品が特に学会側の人に評価された理由として、
・研究っぽい上に学会でもやられていないこと
・それをポップな体験に落とし込んでいること
・サイバネっぽい外見、テーマ、フィードバックシステム
・勿論現実には存在しない体験と、良いモチーフを用いることによる納得感

が考えられました。

 

過去の優勝・準優勝作品なども踏まえて考えると、IVRC作品は以下のようなポイントを踏まえると良い感じになると思われます。

 

[必須]

  • ポップさ(体験の魅力)
  • 現実の下位互換ではないVR的な価値
    • もしくは現実では中々出来ない体験
  • 新奇性
  • (技術的な)面白さ

[あればいい]

  • キュレーション
  • マルチモーダル刺激
  • シナリオ
  • 操作性
  • 対人性
  • ゲーム性

[隠し味]

  • サイバネっぽさ
  • SDGsとか地方創生みたいな良い価値
  • (VRでしか出来ない)タブー感

 

 

ということで、Mechanical Brain Hackingのメイキングでした。

VRで(研究でなく)ガッツリとしたコンテンツを作るのは初めてでしたが、デモ会場でも良い感想を多く頂け、自分としてもとても嬉しいです。多くの方に楽しんで頂けたようで何よりです!

自分は絵作りのセンスはあまり無いのですが、丁度良いタイミングでお絵描きAIが台頭してきて幸運でした。ケレン味のあるシーンに仕上がったのではないかと思います。

 

今後の展示に向けては、要素の拡充とオペレーションの改善を行っていきます。

I-CAR、XR Kaigi、東京大学製作展、Laval Virtualなどにも出展しますので、機会があれば体験してください。

また、当作品は国際会議SIGGRAPH'23 Immersive Pavilionにも採択されたので、ロサンゼルスでも展示を行ってくる予定です。


この記事がVRコンテンツ作成の参考になれば幸いです。

よきVRライフを!

Unityでx軸方向だけのrotationやlocalRotationをコピーしたい

Unityで、targetのgameobjectから別のgameobjectへx軸方向だけの回転をコピーしたい時があると思う。

素朴な実装では、

    private void RotUpdateX(ref GameObject me, GameObject target){
        var r = target.transform.localRotation;
      r = Quaternion.Euler(r.eulerAngles.x,0,0);
      me.transform.localRotation = r;
    }

と書く人が多いだろうが、これでは上手くいかなかった。

quaternion.eulerAnglesにおいてquaternionからオイラー角に変換するときに、同じ回転を表す別のオイラー角に変換されることが原因らしい。

qiita.com

自分の場合は、以下のようにしたら上手くいった。

    private void RotUpdateX(ref GameObject me, GameObject target){
        var r = target.transform.localRotation;
        if (r.eulerAngles.y > 90 & r.eulerAngles.y < 270)
        {
            r = Quaternion.Euler(180 - r.eulerAngles.x,0,0);

        }else{
            r = Quaternion.Euler(r.eulerAngles.x,0,0);

      }
      me.transform.localRotation = r;
  }