20191201のUnityに関する記事は16件です。

ARKit 3のMotion CaptureでVRMを動かす【Unity】

はじめに

この記事はVTuber Tech #1 Advent Calendar 2019 1日目の記事です。

さて、iOSのARKit 3でMotion Captureの機能が追加されました。

UnityではARを扱うためのフリームワークであるAR FoundationでARKit 3の機能が使えるということで、早速試してみました。今回はVRM形式のウチの子『リリカちゃん』をARKit 3を使って動かして行こうと思います。

環境

ARKitにはいくつかの制約があり、Motion Captureを含む以下の機能の使用にはA12チップを搭載したデバイスである必要があります。つまりiPhone XS/XR以降の端末でしか使えません。

  • People Occlusion
  • Motion capture
  • フロントカメラ/バックカメラの同時使用
  • 複数フェイストラッキング

People Occlusion and the use of motion capture, simultaneous front and back camera, and multiple face tracking are supported on devices with A12/A12X Bionic chips, ANE, and TrueDepth Camera.

参考: ARKit 3 - Augmented Reality - Apple Developer

また、Motion Captureはバックカメラでしか動作しないようです。

When ARKit identifies a person in the back camera feed, it calls session(_:didAdd:), passing you an ARBodyAnchor you can use to track the body's movement.

参考: ARBodyTrackingConfiguration - ARKit | Apple Developer Documentation

また、AR Foundationについてもはまりどころがありました。

macOS CatalinaにおけるUnityの既知のバグで、iOS向けのビルドの描画がおかしくなる問題がありました。

Can't launch ARCollaborationData iOS · Issue #325 · Unity-Technologies/arfoundation-samples

こちらで示されているIssue Trackerによると2020.1以降で修正済みということで、今回はα版ですがUnity 2020.1.0a14.1541を使って実装してみました。
https://issuetracker.unity3d.com/issues/ios

アプローチ

Unity-Technologies/arfoundation-samplesをベースに実装しました。

上手くいかなかった方法

読み飛ばしてもらって問題ないです。m_HumanBodyManager.humanBodiesChangedから取得できる関節情報を直接Humanoid方に流し込む方法を試しました。コードはこんな感じ。

HumanoidTracker.cs
// VRM生成部分は省略
public class HumanoidTracker : MonoBehaviour
{
    [SerializeField] ARHumanBodyManager m_HumanBodyManager;

    private Animator _animator;

    void OnEnable()
    {
        m_HumanBodyManager.humanBodiesChanged += OnHumanBodiesChanged;
    }

    void OnDisable()
    {
        if (m_HumanBodyManager != null)
        {
            m_HumanBodyManager.humanBodiesChanged -= OnHumanBodiesChanged;
        }
    }

    private void Update()
    {
        var origin = FindObjectOfType<BoneController>()?.GetComponent<Animator>();
        if (_animator == null || origin == null)
        {
            return;
        }

        var originalHandler = new HumanPoseHandler(origin.avatar, origin.transform);
        var targetHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);

        HumanPose humanPose = new HumanPose();
        originalHandler.GetHumanPose(ref humanPose);
        targetHandler.SetHumanPose(ref humanPose);

        _animator.rootPosition = origin.rootPosition;
        _animator.rootRotation = origin.rootRotation;
    }

    void OnHumanBodiesChanged(ARHumanBodiesChangedEventArgs eventArgs)
    {
        if (_animator == null)
        {
            return;
        }

        foreach (var humanBody in eventArgs.updated)
        {
            SetHumanBoneTransformToHumanoidPoses(humanBody);
        }
    }

    void SetHumanBoneTransformToHumanoidPoses(ARHumanBody body)
    {
        if (!body.joints.IsCreated)
        {
            return;
        }

        var bones = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[];
        foreach (HumanBodyBones bone in bones)
        {
            if (bone < 0 || bone >= HumanBodyBones.LastBone)
            {
                continue;
            }

            var joint = HumanoidUtils.GetXRHumanBodyJoint(body, bone);
            Transform t = _animator.GetBoneTransform(bone);
            if (t != null)
            {
                t.localPosition = joint.localPose.position;
                t.localRotation = joint.localPose.rotation;
            }
        }
    }
}

前半はほぼサンプルコードそのままで、注目して欲しいのは後半のSetHumanBoneTransformToHumanoidPosesメソッドです。Unity標準Humanoidアバターのボーン構造であるHumanBodyBonesforeachで回して、ジョイントの値を取得しています。

void SetHumanBoneTransformToHumanoidPoses(ARHumanBody body)
{
    if (!body.joints.IsCreated)
    {
        return;
    }

    var bones = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[];
    foreach (HumanBodyBones bone in bones)
    {
        if (bone < 0 || bone >= HumanBodyBones.LastBone)
        {
            continue;
        }

        var joint = HumanoidUtils.GetXRHumanBodyJoint(body, bone);
        Transform t = _animator.GetBoneTransform(bone);
        if (t != null)
        {
            t.localPosition = joint.localPose.position;
            t.localRotation = joint.localPose.rotation;
        }
    }
}

ARKit 3で取得できるジョイントは約90関節で、対するHumanoid型は訳54関節なので、対応付けをする必要があります。エディタ上で比較しながら対応させるメソッドを書きました。

HumanoidUtils.cs
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public static class HumanoidUtils
{
    // 3D joint skeleton
    enum JointIndices
    {
        Invalid = -1,
        Root = 0, // parent: <none> [-1]
        Hips = 1, // parent: Root [0]
        LeftUpLeg = 2, // parent: Hips [1]
        LeftLeg = 3, // parent: LeftUpLeg [2]
        LeftFoot = 4, // parent: LeftLeg [3]
        LeftToes = 5, // parent: LeftFoot [4]
        LeftToesEnd = 6, // parent: LeftToes [5]
        RightUpLeg = 7, // parent: Hips [1]
        RightLeg = 8, // parent: RightUpLeg [7]
        RightFoot = 9, // parent: RightLeg [8]
        RightToes = 10, // parent: RightFoot [9]
        RightToesEnd = 11, // parent: RightToes [10]
        Spine1 = 12, // parent: Hips [1]
        Spine2 = 13, // parent: Spine1 [12]
        Spine3 = 14, // parent: Spine2 [13]
        Spine4 = 15, // parent: Spine3 [14]
        Spine5 = 16, // parent: Spine4 [15]
        Spine6 = 17, // parent: Spine5 [16]
        Spine7 = 18, // parent: Spine6 [17]
        LeftShoulder1 = 19, // parent: Spine7 [18]
        LeftArm = 20, // parent: LeftShoulder1 [19]
        LeftForearm = 21, // parent: LeftArm [20]
        LeftHand = 22, // parent: LeftForearm [21]
        LeftHandIndexStart = 23, // parent: LeftHand [22]
        LeftHandIndex1 = 24, // parent: LeftHandIndexStart [23]
        LeftHandIndex2 = 25, // parent: LeftHandIndex1 [24]
        LeftHandIndex3 = 26, // parent: LeftHandIndex2 [25]
        LeftHandIndexEnd = 27, // parent: LeftHandIndex3 [26]
        LeftHandMidStart = 28, // parent: LeftHand [22]
        LeftHandMid1 = 29, // parent: LeftHandMidStart [28]
        LeftHandMid2 = 30, // parent: LeftHandMid1 [29]
        LeftHandMid3 = 31, // parent: LeftHandMid2 [30]
        LeftHandMidEnd = 32, // parent: LeftHandMid3 [31]
        LeftHandPinkyStart = 33, // parent: LeftHand [22]
        LeftHandPinky1 = 34, // parent: LeftHandPinkyStart [33]
        LeftHandPinky2 = 35, // parent: LeftHandPinky1 [34]
        LeftHandPinky3 = 36, // parent: LeftHandPinky2 [35]
        LeftHandPinkyEnd = 37, // parent: LeftHandPinky3 [36]
        LeftHandRingStart = 38, // parent: LeftHand [22]
        LeftHandRing1 = 39, // parent: LeftHandRingStart [38]
        LeftHandRing2 = 40, // parent: LeftHandRing1 [39]
        LeftHandRing3 = 41, // parent: LeftHandRing2 [40]
        LeftHandRingEnd = 42, // parent: LeftHandRing3 [41]
        LeftHandThumbStart = 43, // parent: LeftHand [22]
        LeftHandThumb1 = 44, // parent: LeftHandThumbStart [43]
        LeftHandThumb2 = 45, // parent: LeftHandThumb1 [44]
        LeftHandThumbEnd = 46, // parent: LeftHandThumb2 [45]
        Neck1 = 47, // parent: Spine7 [18]
        Neck2 = 48, // parent: Neck1 [47]
        Neck3 = 49, // parent: Neck2 [48]
        Neck4 = 50, // parent: Neck3 [49]
        Head = 51, // parent: Neck4 [50]
        Jaw = 52, // parent: Head [51]
        Chin = 53, // parent: Jaw [52]
        LeftEye = 54, // parent: Head [51]
        LeftEyeLowerLid = 55, // parent: LeftEye [54]
        LeftEyeUpperLid = 56, // parent: LeftEye [54]
        LeftEyeball = 57, // parent: LeftEye [54]
        Nose = 58, // parent: Head [51]
        RightEye = 59, // parent: Head [51]
        RightEyeLowerLid = 60, // parent: RightEye [59]
        RightEyeUpperLid = 61, // parent: RightEye [59]
        RightEyeball = 62, // parent: RightEye [59]
        RightShoulder1 = 63, // parent: Spine7 [18]
        RightArm = 64, // parent: RightShoulder1 [63]
        RightForearm = 65, // parent: RightArm [64]
        RightHand = 66, // parent: RightForearm [65]
        RightHandIndexStart = 67, // parent: RightHand [66]
        RightHandIndex1 = 68, // parent: RightHandIndexStart [67]
        RightHandIndex2 = 69, // parent: RightHandIndex1 [68]
        RightHandIndex3 = 70, // parent: RightHandIndex2 [69]
        RightHandIndexEnd = 71, // parent: RightHandIndex3 [70]
        RightHandMidStart = 72, // parent: RightHand [66]
        RightHandMid1 = 73, // parent: RightHandMidStart [72]
        RightHandMid2 = 74, // parent: RightHandMid1 [73]
        RightHandMid3 = 75, // parent: RightHandMid2 [74]
        RightHandMidEnd = 76, // parent: RightHandMid3 [75]
        RightHandPinkyStart = 77, // parent: RightHand [66]
        RightHandPinky1 = 78, // parent: RightHandPinkyStart [77]
        RightHandPinky2 = 79, // parent: RightHandPinky1 [78]
        RightHandPinky3 = 80, // parent: RightHandPinky2 [79]
        RightHandPinkyEnd = 81, // parent: RightHandPinky3 [80]
        RightHandRingStart = 82, // parent: RightHand [66]
        RightHandRing1 = 83, // parent: RightHandRingStart [82]
        RightHandRing2 = 84, // parent: RightHandRing1 [83]
        RightHandRing3 = 85, // parent: RightHandRing2 [84]
        RightHandRingEnd = 86, // parent: RightHandRing3 [85]
        RightHandThumbStart = 87, // parent: RightHand [66]
        RightHandThumb1 = 88, // parent: RightHandThumbStart [87]
        RightHandThumb2 = 89, // parent: RightHandThumb1 [88]
        RightHandThumbEnd = 90, // parent: RightHandThumb2 [89]
    }

    public static XRHumanBodyJoint GetXRHumanBodyJoint(ARHumanBody body, HumanBodyBones bone)
    {
        switch (bone)
        {
            case HumanBodyBones.Hips:
                return body.joints[(int)JointIndices.Hips];
            case HumanBodyBones.LeftUpperLeg:
                return body.joints[(int)JointIndices.LeftUpLeg];
            case HumanBodyBones.RightUpperLeg:
                return body.joints[(int)JointIndices.RightUpLeg];
            case HumanBodyBones.LeftLowerLeg:
                return body.joints[(int)JointIndices.LeftLeg];
            case HumanBodyBones.RightLowerLeg:
                return body.joints[(int)JointIndices.RightLeg];
            case HumanBodyBones.LeftFoot:
                return body.joints[(int)JointIndices.LeftFoot];
            case HumanBodyBones.RightFoot:
                return body.joints[(int)JointIndices.RightFoot];
            case HumanBodyBones.Spine:
                return body.joints[(int)JointIndices.Spine1];
            case HumanBodyBones.Chest:
                return body.joints[(int)JointIndices.Spine6];
            case HumanBodyBones.UpperChest:
                return body.joints[(int)JointIndices.Spine7];
            case HumanBodyBones.Neck:
                return body.joints[(int)JointIndices.Neck1];
            case HumanBodyBones.Head:
                return body.joints[(int)JointIndices.Head];
            case HumanBodyBones.LeftShoulder:
                return body.joints[(int)JointIndices.LeftShoulder1];
            case HumanBodyBones.RightShoulder:
                return body.joints[(int)JointIndices.RightShoulder1];
            case HumanBodyBones.LeftUpperArm:
                return body.joints[(int)JointIndices.LeftArm];
            case HumanBodyBones.RightUpperArm:
                return body.joints[(int)JointIndices.RightArm];
            case HumanBodyBones.LeftLowerArm:
                return body.joints[(int)JointIndices.LeftForearm];
            case HumanBodyBones.RightLowerArm:
                return body.joints[(int)JointIndices.RightForearm];
            case HumanBodyBones.LeftHand:
                return body.joints[(int)JointIndices.LeftHand];
            case HumanBodyBones.RightHand:
                return body.joints[(int)JointIndices.RightHand];
            case HumanBodyBones.LeftToes:
                return body.joints[(int)JointIndices.LeftToes];
            case HumanBodyBones.RightToes:
                return body.joints[(int)JointIndices.RightToes];
            case HumanBodyBones.LeftEye:
                return body.joints[(int)JointIndices.LeftEye];
            case HumanBodyBones.RightEye:
                return body.joints[(int)JointIndices.RightEye];
            case HumanBodyBones.Jaw:
                return body.joints[(int)JointIndices.Jaw];
            case HumanBodyBones.LeftThumbProximal:
                return body.joints[(int)JointIndices.LeftHandThumbStart];
            case HumanBodyBones.LeftThumbIntermediate:
                return body.joints[(int)JointIndices.LeftHandThumb1];
            case HumanBodyBones.LeftThumbDistal:
                return body.joints[(int)JointIndices.LeftHandThumb2];
            case HumanBodyBones.LeftIndexProximal:
                return body.joints[(int)JointIndices.LeftHandIndex1];
            case HumanBodyBones.LeftIndexIntermediate:
                return body.joints[(int)JointIndices.LeftHandIndex2];
            case HumanBodyBones.LeftIndexDistal:
                return body.joints[(int)JointIndices.LeftHandIndex3];
            case HumanBodyBones.LeftMiddleProximal:
                return body.joints[(int)JointIndices.LeftHandMid1];
            case HumanBodyBones.LeftMiddleIntermediate:
                return body.joints[(int)JointIndices.LeftHandMid2];
            case HumanBodyBones.LeftMiddleDistal:
                return body.joints[(int)JointIndices.LeftHandMid3];
            case HumanBodyBones.LeftRingProximal:
                return body.joints[(int)JointIndices.LeftHandRing1];
            case HumanBodyBones.LeftRingIntermediate:
                return body.joints[(int)JointIndices.LeftHandRing2];
            case HumanBodyBones.LeftRingDistal:
                return body.joints[(int)JointIndices.LeftHandRing3];
            case HumanBodyBones.LeftLittleProximal:
                return body.joints[(int)JointIndices.LeftHandPinky1];
            case HumanBodyBones.LeftLittleIntermediate:
                return body.joints[(int)JointIndices.LeftHandPinky2];
            case HumanBodyBones.LeftLittleDistal:
                return body.joints[(int)JointIndices.LeftHandPinky3];
            case HumanBodyBones.RightThumbProximal:
                return body.joints[(int)JointIndices.RightHandThumbStart];
            case HumanBodyBones.RightThumbIntermediate:
                return body.joints[(int)JointIndices.RightHandThumb1];
            case HumanBodyBones.RightThumbDistal:
                return body.joints[(int)JointIndices.RightHandThumb2];
            case HumanBodyBones.RightIndexProximal:
                return body.joints[(int)JointIndices.RightHandIndex1];
            case HumanBodyBones.RightIndexIntermediate:
                return body.joints[(int)JointIndices.RightHandIndex2];
            case HumanBodyBones.RightIndexDistal:
                return body.joints[(int)JointIndices.RightHandIndex3];
            case HumanBodyBones.RightMiddleProximal:
                return body.joints[(int)JointIndices.RightHandMid1];
            case HumanBodyBones.RightMiddleIntermediate:
                return body.joints[(int)JointIndices.RightHandMid2];
            case HumanBodyBones.RightMiddleDistal:
                return body.joints[(int)JointIndices.RightHandMid3];
            case HumanBodyBones.RightRingProximal:
                return body.joints[(int)JointIndices.RightHandRing1];
            case HumanBodyBones.RightRingIntermediate:
                return body.joints[(int)JointIndices.RightHandRing2];
            case HumanBodyBones.RightRingDistal:
                return body.joints[(int)JointIndices.RightHandRing3];
            case HumanBodyBones.RightLittleProximal:
                return body.joints[(int)JointIndices.RightHandPinky1];
            case HumanBodyBones.RightLittleIntermediate:
                return body.joints[(int)JointIndices.RightHandPinky2];
            case HumanBodyBones.RightLittleDistal:
                return body.joints[(int)JointIndices.RightHandPinky3];
            default:
                return body.joints[(int)JointIndices.Invalid];
        }
    }
}

これを動かした結果がこれ。

原因はよく考えたら当たり前で、サンプルに含まれているモデルと、使用したVRMのボーン構造が異なるからです。Animatorで用いるリグ構造は親のボーンを基準として相対姿勢で表現するため、異なるボーン構造のモデルには適用できません。XRHumanBodyJointで取得できる値には、親ボーンからの相対姿勢を取得するLocalPoseの他に、ルートボーンからの相対姿勢を表すAnchorPoseがあるのすが、こちらも各関節の角度が一致しているモデルでないと使用できません。ただし、その差分を計測できれば可能性はありそうでした。

しかし、Issueによるとこの値はARKit 3の生の値ではないらしいです。そのため頑張って対応しても今後変更される可能性があり、あまり分が良い方法ではなさそうです。

参考: Example Rig for 3D Human Skeleton - Unity Forum

そこで、次の方法を試しました。

上手く行った方法

ほぼこのツイートの通りにしたら上手くいきました。

具体的には

  1. Package Managerで「FBX Exporter」をInstall
  2. Assets/Prefabs/Robot/ControlledRobot.prefabの上で右クリック
  3. Convert To FBX Linked Prefabを選択肢、Convertする
  4. 出力されたfbxを選択
  5. インスペクターでRigタブに切り替え、Animation TypeHumanoidに変更
  6. Configureを選択
  7. 大体いい感じに自動で設定してくれているが、数カ所おかしいので以下のように修正(Hierarchy Viewからドラッグアンドドロップで設定しないと親がおかしいと怒られた) スクリーンショット 2019-12-02 0.07.44.png スクリーンショット 2019-12-02 0.07.49.png スクリーンショット 2019-12-02 0.07.55.png
  8. Applyして終了
  9. プレハブしてBoneControllerをアタッチ
  10. Human Body TrackingSkelton Prefabに上記プレハブを設定
  11. 以下のスクリプトをアタッチして完成
HumanoidTracker.cs
using System.IO;
using UnityEngine;
using VRM;

public class HumanoidTracker : MonoBehaviour
{
    private Animator _animator;

    private void Start()
    {
        ImportVRMAsync();
    }

    private void Update()
    {
        var origin = FindObjectOfType<BoneController>()?.GetComponent<Animator>();
        if (_animator == null || origin == null)
        {
            return;
        }

        var originalHandler = new HumanPoseHandler(origin.avatar, origin.transform);
        var targetHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);

        HumanPose humanPose = new HumanPose();
        originalHandler.GetHumanPose(ref humanPose);
        targetHandler.SetHumanPose(ref humanPose);

        _animator.rootPosition = origin.rootPosition;
        _animator.rootRotation = origin.rootRotation;
    }

    private void ImportVRMAsync()
    {
        //VRMファイルのパスを指定します
        var path = $"{Application.streamingAssetsPath}/lyrica_chloma.vrm";

        //ファイルをByte配列に読み込みます
        var bytes = File.ReadAllBytes(path);

        //VRMImporterContextがVRMを読み込む機能を提供します
        var context = new VRMImporterContext();

        // GLB形式でJSONを取得しParseします
        context.ParseGlb(bytes);

        // VRMのメタデータを取得
        var meta = context.ReadMeta(false); //引数をTrueに変えるとサムネイルも読み込みます

        //読み込めたかどうかログにモデル名を出力してみる
        Debug.LogFormat("meta: title:{0}", meta.Title);

        //非同期処理で読み込みます
        context.LoadAsync(_ => OnLoaded(context));
    }

    private void OnLoaded(VRMImporterContext context)
    {
        //読込が完了するとcontext.RootにモデルのGameObjectが入っています
        var root = context.Root;

        _animator = root.GetComponent<Animator>();
        root.transform.position = new Vector3(0, -1, 1);
        root.transform.rotation = Quaternion.Euler(0, 180f, 0);
        _animator.applyRootMotion = true;

        //メッシュを表示します
        context.ShowMeshes();
    }
}

参考: UniVRMを使ってVRMモデルをランタイムロードする方法

やっていることはさっきよりシンプルです。サンプルに含まれているモデルをHumanoid型として扱えるようになったので、Humanoid型としてVRMに値を流し込んでいます。その際にHumanPoseHandlerを経由する必要がある点に注意してください。

private void Update()
{
    var origin = FindObjectOfType<BoneController>()?.GetComponent<Animator>();
    if (_animator == null || origin == null)
    {
        return;
    }

    var originalHandler = new HumanPoseHandler(origin.avatar, origin.transform);
    var targetHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);

    HumanPose humanPose = new HumanPose();
    originalHandler.GetHumanPose(ref humanPose);
    targetHandler.SetHumanPose(ref humanPose);

    _animator.rootPosition = origin.rootPosition;
    _animator.rootRotation = origin.rootRotation;
}

参考: ランタイムでAvatarを生成してアニメーションに利用する - e.blog

上記の実装をした結果がこんな感じです。

大分いい感じですね。ルート位置をロボットモデルと同じように変更する方法が分かってないので、わかる人がいたら教えて欲しいです。

最後に

いろいろ遠回りをしてしまいましたが、Humanoid型は異なる階層構造の人型モデルを同じように扱えることが分かりました。現状だと精度の面や使いやすさの面で難はありますが、デファクトの機能でモーキャプが簡単に扱えるのは面白いですね。

明日以降のVTuber Tech #1 Advent Calendar 2019も是非お楽しみに!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityのTimelineでクリップが一つだけのトラックを作る

こんにちは、ZeniZeniです。
最近Unityのタイムラインをよくいじっています。

やりたいこと

Unityのタイムラインをいじっていると、トラック上にクリップが一つだけしか存在してはいけないような制限を設けたいときがあります。(私はありました。)

さぁ作ろうと思って色々調べてみると、カスタムPlayable Trackでタイムラインのトラックを自作しないといけないことがわかりました。
こちらの動画が参考になります。
https://youtu.be/6SPpjSKy9LI

上の動画を参考にしながら、トラックにクリップが二つ以上置けない制限を設けた、シーン遷移を行うカスタムトラックを作成しました。
プロジェクトはこちらにあります。
https://github.com/Zeni-Y/TimelineCustomTrackTest

実装方法

まず、作製したMixerとTrackのコードはこちらです。(プラスClipとBehaviourのコードもありますが、長くなるので割愛します)

SceneManagementMixer
using System.Collections.Generic;
using UnityEngine.Playables;
using UnityEngine.Timeline;
using System.Linq;
using UnityEngine.SceneManagement;

namespace ZeniZeni.CustomTrack
{
    public class SceneManagementMixer : PlayableBehaviour
    {

        internal PlayableDirector m_playableDirector;

        internal string m_scene;
        internal LoadSceneMode m_mode;

        internal IEnumerable<TimelineClip> m_clips;
        private bool oneShot = true;

        // NOTE: This function is called at runtime and edit time.  Keep that in mind when setting the values of properties.
        public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {
            if (m_clips.Count() >= 2) return;

            int inputCount = playable.GetInputCount<Playable>();
            if (inputCount == 0) return;

            var time = m_playableDirector.time;
            var enumulator = m_clips.GetEnumerator();
            enumulator.MoveNext();

            for (int i = 0; i < inputCount; i++, enumulator.MoveNext())
            {
                var clip = enumulator.Current;

                var asset = clip.asset as SceneManagementClip;

                m_scene = asset.m_scene;
                m_mode = asset.m_mode;
                if (clip.start <= time && time <= clip.end  && oneShot)
                {
                    SceneManager.LoadSceneAsync(m_scene, m_mode);
                    oneShot = false;
                }
                else if (time < clip.start || time > clip.end)
                {
                    if (!oneShot) oneShot = true;
                }
            }

        }
    }
}
SceneManagementTrack.cs
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace ZeniZeni.CustomTrack
{
    [TrackColor(0.3523021f, 1f, 0f)]
    [TrackClipType(typeof(SceneManagementClip))]
    public class SceneManagementTrack : TrackAsset
    {

        public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
        {

            var mixer = ScriptPlayable<SceneManagementMixerBehaviour>.Create(graph, inputCount);
            var director = go.GetComponent<PlayableDirector>();
            if (director != null)
            {
                SceneManagementMixerBehaviour bh = mixer.GetBehaviour();
                //Track上のクリップを取得
                bh.m_clips = GetClips();

                //Clipが二つ以上あれば一つだけになるようにClipを新しい順に消す
                if (bh.m_clips.Count() >= 2)
                {
                    for (int i = 0; i < bh.m_clips.Count()-1; i++)
                    {
                        timelineAsset.DeleteClip(bh.m_clips.Last());
                    }
                    Debug.Log("You can put only one SceneManagementClip in this Track.");
                }
                bh.m_playableDirector = director;
            }

            return mixer;
        }
    }
}

クリップを二つ以上置けないようにする機能の実装方法ですが、クリップを作成したときに、そのトラック上にクリップが二つ以上存在すれば、一つだけになるようにClipを新しい順に消すような形で実装しています。

クリップを二つ以上置こうとすると以下のGIFのようになります。
ダウンロード.gif

ただ問題がありまして、インスペクターを表示した状態でこの動作を行うと、下の画像のようなエラーが発生します。
bandicam 2019-12-01 23-32-49-404.jpg

一応Clearで簡単に消せますが、気になっちゃいますよね。

理想としては、クリップが一つ存在すれば追加でクリップを作成できないような形にしたかったんですが、実装方法がわからず断念しました。
方法がわかり次第、追記していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アスペクト比次第で変わる水平視野を揃える

アス比が違うと起きること


プライドをかけたタイマンだ!
と思ってたけどiPadでプレイしたら…

 … 

なんだねキミたちは!?

と、いうようなことが起きます。
これは 一人用ゲームならアス比の違いでゲームの難易度が変わってしまう し、対戦ゲームなら視野の違いで情報格差が出て有利不利が変わってしまいます
ゲーマーならこの不公平、許せねえ…!

なんでこうなるの?

端末のアスペクト比が変わったとき、画角 …すなわちCameraコンポーネントの FieldOfView (以下、FOV)は変わらないせいです。
UnityのカメラのFOVの値は 垂直視野 であり、水平FOVは画面のアス比によって変わります。(公式リファレンスより
その結果、アスペクト比が横に伸びた時にFOVが同じ値だと、純粋に視野が横に広がります。

そもそもiPadのようなタブレットはディスプレイサイズが大きく普通より画面が広く使える分、より多くの要素を画面に映して快適にアプリが使用できるべき ではあるのですが、前例のとおり、ことゲームに関してはそれだとマズいときがあります。

それを解決するためには、アス比によって画角を変える 必要があります。

視野を揃える

 

揃いました。

忙しいあなたに先に手段を説明!

■スクリプトを用意
HorizontalFOVFitter.cs
using UnityEngine;

/// <summary>
/// 水平視野を合わせる機能
/// </summary>
public class HorizontalFOVFitter : MonoBehaviour {
    /// <summary>カメラ</summary>
    [SerializeField]
    private Camera _camera = null;
    /// <summary>水平視野合わせ機能をOFFにする(垂直視野を合わせる)か否か</summary>
    [SerializeField]
    private bool _isDisable = false;
    /// <summary>基準にするアスペクト比(解像度での指定も可)</summary>
    [SerializeField]
    private Vector2 _baseAspect = new Vector2(750, 1334);
    /// <summary>基準にするCameraのFOV(垂直視野)</summary>
    [SerializeField]
    private float _baseFieldOfView = 60f;

    private void Update() {
        if (_camera == null) {
            return;
        }
        if (_isDisable) {
            // 機能を無効化しているときは、通常通り垂直視野をしてFOVを反映する
            if (_camera.fieldOfView != _baseFieldOfView) {
                _camera.fieldOfView = _baseFieldOfView;
            }
            return;
        }
        // 基準の垂直視野とアスペクト比から、基準にする水平視野を計算する
        float baseHorizontalFOV = CalcHorizontalFOV(_baseFieldOfView, CalcAspect(_baseAspect.x, _baseAspect.y));
        float currentAspect = CalcAspect(Screen.width, Screen.height);
        // 基準にする水平視野と現在のアスペクト比から、反映すべき垂直視野を計算する
        _camera.fieldOfView = CalcVerticalFOV(baseHorizontalFOV, currentAspect);
    }

    /// <summary>
    /// アスペクト比を計算する
    /// </summary>
    private float CalcAspect(float width, float height) {
        return width / height;
    }

    /// <summary>
    /// 垂直視野とアスペクト比から、水平視野を計算する
    /// </summary>
    private float CalcHorizontalFOV(float verticalFOV, float aspect) {
        return Mathf.Atan(Mathf.Tan(verticalFOV / 2f * Mathf.Deg2Rad) * aspect) * 2f * Mathf.Rad2Deg;
    }

    /// <summary>
    /// 水平視野とアスペクト比から、垂直視野を計算する
    /// </summary>
    private float CalcVerticalFOV(float horizontalFOV, float aspect) {
        return Mathf.Atan(Mathf.Tan(horizontalFOV / 2f * Mathf.Deg2Rad) / aspect) * 2f * Mathf.Rad2Deg;
    }
}
■Cameraにコンポーネント設定

image.png

  • Base Aspect に、基準にするアス比(もしくは解像度)
  • Base Field Of View に、基準にする画角

を設定して、ゲームを実行すれば完了!

理屈

こちらのteratailの質問への回答が非常に参考になりました!

こちらの解説をもとに、以下の公式を作り、

  • アスペクト比
  • 垂直視野画角(CameraのFOV)
  • 水平視野画角

の3要素のうち、アスペクト比と1つの情報があれば残りの1要素を算出できるようにしました。
その公式は以下の通りです。

水平視野画角の公式
Atan(Tan(垂直視野画角/ 2) * アスペクト比) * 2
垂直視野画角の公式
Atan(Tan(水平視野画角/ 2) / アスペクト比) * 2

後は、

  1. 基準の垂直視野と基準のアスペクト比から、基準にする水平視野を計算する
  2. 基準にする水平視野と現在のアスペクト比から、反映すべき垂直視野を計算する
  3. カメラのFieldOfViewに垂直視野の値を設定する

で完成です!

まとめ

この方法で、アスペクト比の違いに影響されず左右の視野を統一することができました!
ただし、今度はアスペクト比が横長になるほど縦の視野が狭まるようになるため、アスペクトの横の比率が低い時だけ本実装を適用する というような仕様で使うことも考えたほうがいいかもしれません。

遊びやすさと公平性をうまくバランス取って視野制御を!

使用デザイン

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

横方向の視界を揃えて公平なゲームを作るぞ、という話

アス比が違うと起きること


プライドをかけたタイマンだ!
と思ってたけどiPadでプレイしたら…

 … 

なんだねキミたちは!?

と、いうようなことが起きます。
これは 一人用ゲームならアス比の違いでゲームの難易度が変わってしまう し、対戦ゲームなら視野の違いで情報格差が出て有利不利が変わってしまいます
ゲーマーならこの不公平、許せねえ…!

なんでこうなるの?

端末のアスペクト比が変わったとき、画角 …すなわちCameraコンポーネントの FieldOfView (以下、FOV)は変わらないせいです。
UnityのカメラのFOVの値は 垂直視野 であり、水平FOVは画面のアス比によって変わります。(公式リファレンスより
その結果、アスペクト比が横に伸びた時にFOVが同じ値だと、純粋に視野が横に広がります。

そもそもiPadのようなタブレットはディスプレイサイズが大きく普通より画面が広く使える分、より多くの要素を画面に映して快適にアプリが使用できるべき ではあるのですが、前例のとおり、ことゲームに関してはそれだとマズいときがあります。

それを解決するためには、アス比によって画角を変える 必要があります。

視野を揃える

 

揃いました。

忙しいあなたに先に手段を説明!

■スクリプトを用意
HorizontalFOVFitter.cs
using UnityEngine;

/// <summary>
/// 水平視野を合わせる機能
/// </summary>
public class HorizontalFOVFitter : MonoBehaviour {
    /// <summary>カメラ</summary>
    [SerializeField]
    private Camera _camera = null;
    /// <summary>水平視野合わせ機能をOFFにする(垂直視野を合わせる)か否か</summary>
    [SerializeField]
    private bool _isDisable = false;
    /// <summary>基準にするアスペクト比(解像度での指定も可)</summary>
    [SerializeField]
    private Vector2 _baseAspect = new Vector2(750, 1334);
    /// <summary>基準にするCameraのFOV(垂直視野)</summary>
    [SerializeField]
    private float _baseFieldOfView = 60f;

    private void Update() {
        if (_camera == null) {
            return;
        }
        if (_isDisable) {
            // 機能を無効化しているときは、通常通り垂直視野をしてFOVを反映する
            if (_camera.fieldOfView != _baseFieldOfView) {
                _camera.fieldOfView = _baseFieldOfView;
            }
            return;
        }
        // 基準の垂直視野とアスペクト比から、基準にする水平視野を計算する
        float baseHorizontalFOV = CalcHorizontalFOV(_baseFieldOfView, CalcAspect(_baseAspect.x, _baseAspect.y));
        float currentAspect = CalcAspect(Screen.width, Screen.height);
        // 基準にする水平視野と現在のアスペクト比から、反映すべき垂直視野を計算する
        _camera.fieldOfView = CalcVerticalFOV(baseHorizontalFOV, currentAspect);
    }

    /// <summary>
    /// アスペクト比を計算する
    /// </summary>
    private float CalcAspect(float width, float height) {
        return width / height;
    }

    /// <summary>
    /// 垂直視野とアスペクト比から、水平視野を計算する
    /// </summary>
    private float CalcHorizontalFOV(float verticalFOV, float aspect) {
        return Mathf.Atan(Mathf.Tan(verticalFOV / 2f * Mathf.Deg2Rad) * aspect) * 2f * Mathf.Rad2Deg;
    }

    /// <summary>
    /// 水平視野とアスペクト比から、垂直視野を計算する
    /// </summary>
    private float CalcVerticalFOV(float horizontalFOV, float aspect) {
        return Mathf.Atan(Mathf.Tan(horizontalFOV / 2f * Mathf.Deg2Rad) / aspect) * 2f * Mathf.Rad2Deg;
    }
}
■Cameraにコンポーネント設定

image.png

  • Base Aspect に、基準にするアス比(もしくは解像度)
  • Base Field Of View に、基準にする画角

を設定して、ゲームを実行すれば完了!

理屈

こちらのteratailの質問への回答が非常に参考になりました!

こちらの解説をもとに、以下の公式を作り、

  • アスペクト比
  • 垂直視野画角(CameraのFOV)
  • 水平視野画角

の3要素のうち、アスペクト比と1つの情報があれば残りの1要素を算出できるようにしました。
その公式は以下の通りです。

水平視野画角の公式
Atan(Tan(垂直視野画角/ 2) * アスペクト比) * 2
垂直視野画角の公式
Atan(Tan(水平視野画角/ 2) / アスペクト比) * 2

後は、

  1. 基準の垂直視野と基準のアスペクト比から、基準にする水平視野を計算する
  2. 基準にする水平視野と現在のアスペクト比から、反映すべき垂直視野を計算する
  3. カメラのFieldOfViewに垂直視野の値を設定する

で完成です!

まとめ

この方法で、アスペクト比の違いに影響されず左右の視野を統一することができました!
ただし、今度はアスペクト比が横長になるほど縦の視野が狭まるようになるため、アスペクトの横の比率が低い時だけ本実装を適用する というような仕様で使うことも考えたほうがいいかもしれません。

遊びやすさと公平性をうまくバランス取って視野制御を!

使用デザイン

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】Shaderで画像の移動・回転・拡縮(アフィン変換)

この記事は「ちゅらっぷす Advent Calendar 2019」の1日目です。
https://qiita.com/advent-calendar/2019/churapps

環境

  • Unity2018.3.6

やること

  • アフィン変換の行列を使ってShaderで画像を移動・回転・拡縮できるようにする

Unityでエフェクトとか作っているとShaderだけで画像を動かしたくなる場面が結構あったので、これをアフィン変換というなんか凄い変換で解決する

アフィン変換の行列

アフィン変換で移動・回転・拡縮するための行列はそれぞれ以下の通り

移動
\begin{bmatrix}
x \\
y \\
1
\end{bmatrix}
=
\begin{bmatrix}
x' \\
y' \\
1
\end{bmatrix}
\times
\begin{bmatrix}
1 & 0 & 0 \\
0 & 1 & 0 \\
PosX & PosY & 1
\end{bmatrix}
回転
\begin{bmatrix}
x \\
y \\
1
\end{bmatrix}
=
\begin{bmatrix}
x' \\
y' \\
1
\end{bmatrix}
\times
\begin{bmatrix}
cos\theta & sin\theta & 0 \\
-sin\theta & cos\theta & 0 \\
0 & 0 & 1
\end{bmatrix}
拡縮
\begin{bmatrix}
x \\
y \\
1
\end{bmatrix}
=
\begin{bmatrix}
x' \\
y' \\
1
\end{bmatrix}
\times
\begin{bmatrix}
1/ScaleX & 0 & 0 \\
0 & 1/ScaleY & 0 \\
0 & 0 & 1
\end{bmatrix}

これをShader内で書くと以下のようになる

// 移動
float3x3 posisionMatrix = {
  1, 0, 0,
  0, 1, 0,
  _PosX, _PosY, 1
};

// 回転
float3x3 rotateMatrix = {
  cos (_Rotate), sin (_Rotate), 0,
  -sin (_Rotate), cos (_Rotate), 0,
  0, 0, 1
};

// 拡縮
float3x3 scaleMatrix = {
  1/_ScaleX, 0, 0,
  0, 1/_ScaleY, 0,
  0, 0, 1
};

// UVに乗算
float3 mulUV = mul (uv, positionMatrix);
mulUV = mul (uv, rotateMatrix)
mulUV = mul (uv, scaleMatrix);

uv = mulUV;

1.gif

画像の左下が中心になっているので、ピボットの計算も加える

fixed3 pivot = fixed3 (0.5, 0.5, 0.0);

// UVに乗算
float3 mulUV = mul (uv, positionMatrix) - pivot;
mulUV = mul (uv, rotateMatrix)
mulUV = mul (uv, scaleMatrix);

uv = mulUV + pivot;

2.gif

いろいろ加工してみる

移動量に応じてアルファ値を変えてみる

color.a = color.a * (1 - saturate (abs (_PosX)));

3.gif

ピボットからの距離によって回転量を変えてみる

_Rotate = _Rotate * ((sqrt(2) * 0.5) - pow (distance (pivot, uv), 8))

4.gif

てきとうに拡縮

_ScaleY = _ScaleY + (1 - _ScaleY) * (sin (i.uv.x * 180) * 0.5 + 0.5) * _ScaleY;

5.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】【UI】UIまとめ

シーンとUIの関係性について

・1シーンにつき1つのキャンバスが存在する。
・全てのUI要素はキャンバスの子として配置される。

UIのサイズについて

・UIのサイズはキャンバスがデフォルトで持つスクリプトで設定できる。

UIの親子関係

・UIに親子関係を持たせたい場合,空のコンポーネントのオブジェクトの下に子のUIを設置する。
・子のUIのアンカーポイントは親に依存する。

Canvas

キャプチャ.PNG
・UIを立体的に表示させるかといった設定

RenderMode 説明
Overlay カメラに依存せず,レンダリング後にUIが配置される。
Camera カメラの状態によって見た目が変わってくる。
WorldSpace UIがほかのオブジェクトと同様に扱われる。UIのZ座標が大きければ手前のオブジェクトに隠れる。

Canvas Scaler

キャプチャ.PNG

・画面の解像度が変更に伴い,UIのサイズをどう変更するかの設定。

UI Scale Mode 説明
Constant Pixel Size 画面サイズが変わっても, UIのサイズが固定。
Scale With Screen Size 画面のサイズに追従して,UIのサイズが変わる。
Constant Phisical Size 設定した解像度によって,UIのサイズが変わる。

Screen Match Mode (Scale With Screen Sizeの場合)

・参考サイトの動画が非常に参考になる。

Screen Match Mode 説明
Match Width or Height Width = 1の時, 横に縮めるとUIが小さくなるが縦に縮めてもサイズは変わらない。Heightはその逆。
Expand 縦に縮めても,横に縮めてもUIは小さくなる。
Shrink 縦に縮めても,横に縮めてもUIのサイズは変わらない。

まとめ

・UIが位置固定であれば「CanvasのRenderMode 」は「Overlay 」一択
・画面サイズが機種によって異なるスマホでれば「UI Scale Mode」は「Scale With Screen Size」一択
・「Screen Match Mode」は「Match Width or Height」か「Expand」
 「GUIの数が多く、縮小させて表示させる必要があるか」「GUIの数が少なく、縮小させる必要がないか」で
 使い分けるのかという妄想であるが、大抵のスマホゲーは画面狭しとGUIが配置されているため,「Expand」一択でいいかもしれない。

参考サイト

【Unity開発】uGUIのCanvas Scalerの使い方【ひよこエッセンス】
【Unity徹底解説】Canvas Scaler【使い方・スクリプト】

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity] 複数人開発でのTag管理

こんにちは、ZeniZeniです。

問題

最近サークルで複数人開発を経験したんですが、そのときTagやLayerといったProjectSettingsフォルダーに入っているような設定項目の競合でかなり痛い目を見ました。
何が起こったかというと、それらの.assetフォルダはよくある.gitattributeの設定ではLFSのトラッキング対象となっており、バイナリファイルで管理されるようになります。そのため、異なるTag編集を行ったブランチをマージして、コンフリクトが発生したとき、下の画像のように両方の変更を反映させることができなくなります。
bandicam 2019-10-24 17-06-28-167.jpg

しかしUnityプロジェクトをGitHubで管理する場合は、ほとんどの場合でGit LFSを使うと思います。そのため、Git LFSを使ったうえでProjectSettingsフォルダー内のファイルはLFSのトラッキング対象から外して、txtデータで扱う方法が望まれます。
ProjectSettingsフォルダー内のファイルで50MB超えるようなものはない…はず…

対処法

対処法は簡単で、ディレクトリ毎にLFSの適用を変えればいいのです。こちらのサイトが参考になりました。
具体的には、ProjectSettingsフォルダー内にも.gitattributesファイルを新しく作成し、そこに
*.asset !filter !diff !merge text
と書くだけです。
bandicam 2019-12-01 20-24-55-267.jpg

こうすれば、上の画像のようにTagなどで競合が起こった時に、両方の変更をうまく反映させることが可能になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VCIで親子関係っぽい振る舞いをさせてみる(位置と回転

概要

https://qiita.com/120byte/private/b9e5a50db09bcdf9c9c0
があんまりだったので、もう一つ案を考えてみました。

動作

実装

image.png
image.png※CubeA~Bは同じ
image.png※Cube1~3は同じ

local current = ""

function updateAll()
    SetTransform("CubeA", "Cube1")
    SetTransform("CubeB", "Cube2")
    SetTransform("CubeC", "Cube3")
end

function SetTransform(targetName, markerName)
    local target = vci.assets.GetSubItem(targetName)
    local marker = vci.assets.GetSubItem(markerName)
    if current == markerName then
        target.SetPosition(marker.GetPosition())
        target.SetRotation(marker.GetRotation())
    else
        marker.SetPosition(target.GetPosition())
        marker.SetRotation(target.GetRotation())
    end
end

function onGrab(target)
    current = target
end

function onUngrab(target)
    current = ""
end

捕捉

親子関係を持つオブジェクトと、掴んで操作するためのオブジェクトを分けて作ります。親子関係を持つオブジェクトはVCISubItemを付けていません。掴んで操作するオブジェクトを掴んでいるかどうかで、親子オブジェクトと掴みオブジェクト間の位置と回転の取得と設定を入れ替えます。

蛇足

大きさはlocalしかないため一癖あり、用途的にも思い浮かばなかったので諦めてしまいました。(親子関係の大きさから計算すれば合うかな……?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity] ComputeShaderとRenderTextureの連携について

はじめに

RenderTextureに座標情報、速度などその他様々なデータを詰めてComputeShaderで計算を行い、その結果をCPU側で受け取りたいということは多くあると思います.このようなことを実現するためには読み込み可能かつ、書き込み可能なRenderTexture(以後RWTextureという)を用意してデータをそこに詰め込んでいくことが一番楽でしょう.(読み込み用,書き込み用とTextureを分けるのはめんどくさいので..)
しかしUnityではただRenderTextureを生成してそのデータを送信するだけではRWTextureとしてComputeShaderで処理することが出来ないのでそのことについて記述していきます.

ComputeBufferつかえばよくない?

もちろんその通りです.ComputeBufferを使用してRWStructuredBufferとしてComputeShader内で扱うのが一番楽でしょう.自分的にはRenderTextureをCPUとComputeShaderの間での架け橋にしたい時はちゃんとデータが処理されているか確認したい時です.つまりどういうことかというと、UnityではCanvas/RawImageGUI.DrawTextureなどでGameView上へTextureの描画をすることができるのプレビューとしてもRenderTextureを使うことができるということです.

こんな感じでこの例ではAnimation中のモデルの頂点座標と法線方向
bandicam 2019-10-09 20-03-16-040.gif

UnorderedAccessView(UAV)とは

任意の場所にアクセスが出来るTextureのView(フォーマット)のことです.このことは「出力リソースの任意の位置に書き込み可能」ということと等しい意味を持ちます.つまりRWTextureとしてComputeShaderで処理するためにはCPU側でTextureを生成する時にこの設定をしてあげないといけないということです.
また補足とし読み込みしか行えないViewをShaderResourceView(SRV)といいます.

スクリプト

RenderTextureをComputeShaderへ送信してRWTextureとしてComputeShaderで受け取る場合(UAVとしてTextureを扱う)はenableRandomWrite = trueとする必要がある.

dispatch.cs
 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 public class dispatch : MonoBehaviour
 {
     [SerializeField] RenderTexture buffer;
     [SerializeField] ComputeShader cs;
     int kernel;
     void Start()
     {
         buffer.Release();
         buffer.enableRandomWrite = true;
         kernel = cs.FindKernel("calc");
         cs.SetTexture(kernel, "buffer", buffer);
     }

     void Update()
     {
         cs.SetFloat("time", Time.realtimeSinceStartup);
         uint x, y, z;
         cs.GetKernelThreadGroupSizes(kernel, out x, out y, out z);
         cs.Dispatch(kernel, 1, 1, 1);
     }
 }
cs.hlsl
 #pragma kernel calc
 RWTexture2D<float4> buffer;
 float time;

 [numthreads(64,1,1)]
 void calc (uint id : SV_DispatchThreadID)
 {
     float c = frac(time/10.0+float(id/100.0));
     buffer[float2(id, 0.5)] = float4(c, 0.0, 0.0, 1.0);
 }

以上のようなコードを使えばuvの座標に応じてグラデーションのようにBufferの中の情報が更新されていくことがわかるかと思います.

まとめ

ComputeShaderでRWTextureを使いたいならTextureをUAVにする(enableRandomWrite = true)

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rider で捗る、inspector の変数リファクタリング

Unity の IDE に Rider 使うと楽になったな~と思っているショートカットの紹介シリーズです。

前提

  • ショートカットキーの設定が Resharper バインドです

inspector に出てる変数リファクタリング

Unity の inspector に出てる変数のリファクタリングどうしてますか?

  • リファクタリングは諦めている
  • 変数名変更してから、再度設定している
  • FormarlySerializeAs つけて修正している

これ、Rider なら自動 FormarylySerializeAs 付けてでやってくれます。
F2 押して出てくるポップアップに変更後の変数名入れたら一発です。
f2.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blender&VRCメモ(随時更新)

忘れたとき用にメモくらいしようかなと思いました。

メモ① Unity上でスキニング済みのオブジェクトが消えた

Q.BlenderからUnityへfbxを出力した際、SkinnedMeshRendererはあるのに見えなくなりました、なぜ?

A.Blenderでシェイプキーを確認しましょう。ブレンドシェイプがあさっての方向に爆発してる場合があります、爆発してるとBoundsがおかしくなり、描画がされなくなります。

メモ② Blenderで作ったシェイプキーと違う

Q.シェイプキーをUnityで確認しようとすると、Blenderとは違う挙動をしています、なぜ・・・

A.BlenderでFBX出力するときに、「トランスフォーム」→「スケールを適用」の項目を「すべてFBX」にすると治りました。シェイプキーはメッシュの法線いじると法線ベクトルの影響によってUnityでの挙動が変わるらしい、しらんけど。

メモ③ 自作アニメーションを設定したらモデルが埋まる

Q.Blenderで作った3DモデルのアニメーションをUnityに持っていきました、Animation Overrideに設定したらキャラクターが地面に埋まりました、Why...

A.アニメーションファイルFBXのRig設定を「humanoid」に設定してください。FBXのRigの初期状態はGenericです、GenericはTransformコンポーネントのキーフレームになっています、Rigの設定をHumanoidにするとAnimationコンポーネントのキーフレームに切り替わります。

メモ④ Blender2.81でのPie Menuが消えた

Q.Blenderを2.81でアップデートしたらTabのPieMenuが消えました、プリファレンスのアドオンを見たらPieMenuがエラー起きてました、何故。

A.プリファレンス→キーマップ→3DViewの「Tab for Pie Menu」をチェックするとTabのPie Menuが復活します。

メモ⑤ ウェイトのGradient(グラデーション塗り)がうまくいかない。

Q.Blenderのウェイト付け機能のGradientがカクカクになります。
c7ae4b740cda945c7bd1482dd4604980.gif
A.ミラーモディファイアが付いてる可能性があります、モディファイアタブの「リアルタイム」を非アクティブ化して塗りましょう。99303f73773a80a3a85721e3fbcd81dd.gif

最終更新日2019/12/2

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VCIで親子関係っぽい振る舞いをさせてみる(位置だけ……

概要

VCIで親子関係使えないの不便ですよね。ということで位置だけですが、親子関係っぽい振る舞いをさせてみました。

動作

実装

image.png
image.png※Cube1~3は同じ

local current = ""
local relation = {"Cube1", "Cube2", "Cube3"}
local localPos = {Vector3.zero, Vector3.zero}
local index = 1

function updateAll()
    SetPos()
end

function SetPos()
    if current == relation[index] then
        -- 子の位置を移動
        for i = index, #relation - 1 do
            local pos = vci.assets.GetSubItem(relation[i]).GetPosition()
            vci.assets.GetSubItem(relation[i + 1]).SetPosition(pos - localPos[i])
        end
    else
        -- 子の位置を更新
        local parent = vci.assets.GetSubItem(relation[index]).GetPosition()
        local child = vci.assets.GetSubItem(relation[index + 1]).GetPosition()
        localPos[index] = parent - child

        if index < #relation - 1 then
            -- 次の子へ
            index = index + 1
            SetPos()
        else
            index = 1
        end
    end
end

function onGrab(target)
    current = target
end

function onUngrab(target)
    current = ""
end

捕捉

currentは掴んでいるオブジェクトの名前が入るので、掴んでいるオブジェクトの子に対して、位置を移動するか、ローカル座標?の更新をします。relationには先頭から親~子の順で名前を入れておきます。localPosは子になるオブジェクトのローカル座標?を入れます。indexはSetPos関数が再帰なので、relation参照時の要素数として使います。

蛇足

補足の文中のローカル座標に?が付いている辺りでお察し頂ければと思いますが、自信がないので、「こいつ頭悪いなー」程度に眺めて笑ってもらえたらと思います。きっとカッコいい式でもっとスマートに実現できる気がしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity] Androidアプリの64ビット対応でつまづいた話

はじめに

Play StoreにAndroidアプリをあげる際に64ビットにしてね!!と言われ,対応したはずなのに64ビット対応できてないと何度も怒られようやく解消できたので,解決方法をまとめます.

なんで64ビット対応するの?

参考
ざっくりいうと,2021年8月以降64ビットしか扱えないから対応してねという理由.

環境

  • Mac OS Mojave 10.14.6
  • Unity 2018.3.6f1

調べたとおり64ビット対応する

このサイトなどを参考に64ビット対応します.
1. Unity>Preference>External toolsのNDK部分を埋める.
スクリーンショット 2019-11-22 16.51.13.png

  1. Unity>Edit>Project Settings>Player>Configurationで,Script BackendをIL2CPPにして,ARM64にチェックマークを入れる.

スクリーンショット 2019-11-22 16.51.51.png

これでビルドすれば,64ビット対応完了!!!のはずだった...

「このリリースは Google Play の 64 ビット要件に準拠していません」

はい,またこのエラーがでました.64ビット対応できてないらしい.
調べていたらこの記事に出会った.

ほう,X86を入れてビルドするとだめらしい...

"X86"を外してビルドする.

  • "x86"のチェックマークを外す スクリーンショット 2019-11-22 16.51.41.png

これをビルドしたら無事に上げることができました!

これをすると32bitで動いていたものが動かなくなりますが,まぁしょうがないでしょう.

所感

ARM64にチェックマークを入れてね,という記事はあったが,x86を選択するとだめというトラップに見事引っかかってしまった.Unity 2019以降だとそもそもx86がないのでこういうことが起こってしまったのだろう.

しかし無事に対処できて本当に良かった.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityエンジニアがMagic Leap Oneを初めて触る際に参考にした記事

公式

Unity Get Started

API Reference

環境構築

MagicLeap入門以前

[Unity]Unityで始めるMagicLeap開発

動画のキャプチャ

Magic Leap/mldbコマンドのヘルプ

個人的開発メモ

Main Camera

必ず、[Magic Leap]>[Core]>[Prefabs]>[Main Camera]を使用する。
これがある状態でビルドする。
image.png

Audio

[Project Setting]>[Audio]で
[Spatializer Plugin]を MSA Spatializerにする
image.png

https://creator.magicleap.com/learn/guides/gsg-create-your-first-unity-app
を参考にしてます。

(随時更新)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity で離散化波動方程式のシミュレーション!

この記事は UT-virtaul Advent Calendar 2019 の1日目として書かれたものです。

波って良くないですか?

唐突なんですけど、波っていいですよね。最近授業で波に関わる内容をたくさん扱っているのもあって、ちょっと気になっています。
あと、作品を作る際に何らかのインタラクションが欲しいなあって気分になるんですが、物理法則に則った動きが欲しいなとも思っていました。その時に、波のような動きとかちょっと面白いんじゃないかって思ったんです。
ということで、Unityで波のシミュレーションをしたくなったわけです。

シミュレーション何が必要か

というわけで、シミュレーションに何が必要か考えました。その結果、コアになりそうなのは以下だなあと思いました。

  1. 波動方程式の離散化
  2. 離散化した波動方程式をシミュレーション領域でリアルタイムに計算しきるための並列計算

1つ目は純粋に数学をやるという話なのですが、2番目はもうちょっと細分化して考える必要がありそうです。何かないかなーと思っていたところ、Unity Graphics Programming なるちょうどいい本を見つけたので、これに載っていた ComputeShader と GPU Instancing を使ってGPUでゴリゴリ頑張る方法を試してみることにしました。

波動方程式の離散化

ネットで検索するとこれとか、結構たくさん資料があります。それを参考にしつつ、軽く解説します。
まず、連続な波動方程式はこれです。
image.png
これを、離散化することを考えます。
2階微分の離散化は、テイラー展開を使って考えることができるようです。まずは、以下のように分解します。
image.png
そして、その2式を足してあげると、下のような式が出てきます。
image.png
これで、2階微分を離散化することができました。
xについても同様にしてあげて、最初の式に代入し整理してあげると、今回使う式が出てきます。
image.png
2階微分に関しては、t と t-δt という2ステップ前までのデータがあれば計算を進めることができます。
今回はこれの2次元版を使っていきます。

ComputeShaderを使った並列計算

今回必要なスクリプトは

  1. 実際に並列計算をする ComputeShader のComputeWave.compute
  2. ComputeShader に ComputeBuffer を渡してシミュレーションを始めさせるComputeWave.cs
  3. GPU Instancing での描画を担うRenderWave.shader
  4. 計算結果を受け取って、シェーダーに渡し、GPU Instancing をするRenderWave.cs

の4つです。

ComputeWave.cs

using UnityEngine;
using System.Runtime.InteropServices;

public class ComputeWave : MonoBehaviour
{
    //グループのスレッド数
    const int SIMULATION_BLOCK_SIZE = 256;
    //シミュレーションするエリアの広さ
    public Vector2 simulationSizes = Vector2.one * 10;
    //シミュレーションで使うキューブの数
    public Vector2Int simulationResolutions = Vector2Int.one * 256;
    public int simulationResolution { get; private set; }
    //キューブの幅
    public Vector2 positionStep { get; private set; }

    //時間の幅:1/60sで固定
    float timeStep;

    //波速
    [SerializeField] float velocity = 1f;

    //シミュレーションエリア
    public Vector3 wallCenter { get; private set; }
    public Vector3 wallSize { get; private set; }

    //シミュレーションを行うComputeShader
    [SerializeField] ComputeShader WaveCS;

    //0ステップ前の変位のバッファ
    public ComputeBuffer _currentDisplacement { get; private set; }
    //1ステップ前の変位のバッファ
    ComputeBuffer _pastDisplacement;

    private void Start()
    {
        FixParameters();
        InitBuffer();
    }

    private void FixedUpdate()
    {
        Simulate();
    }

    private void OnDestroy()
    {
        ReleaseBuffer();
    }

    //パラメータの初期化
    void FixParameters()
    {
        positionStep = simulationSizes / simulationResolutions;
        simulationResolution = simulationResolutions.x * simulationResolutions.y;
        timeStep = Time.fixedDeltaTime;
        wallCenter = Vector3.zero;
        wallSize = new Vector3(simulationSizes.x, 8f, simulationSizes.y);
    }

    //バッファの初期化
    void InitBuffer()
    {
        _currentDisplacement = new ComputeBuffer(
            simulationResolution, 
            Marshal.SizeOf(typeof(float)));
        _pastDisplacement = new ComputeBuffer(
            simulationResolution,
            Marshal.SizeOf(typeof(float)));

        var currentDisplacementArray = new float[simulationResolution];
        var pastDisplacementArray = new float[simulationResolution];
        for(int i = 0; i < simulationResolution; i++)
        {
            currentDisplacementArray[i] = 0f;
            pastDisplacementArray[i] = 0f;
        }
        //とりあえずエリアの真ん中にちょっと値を入れてみる
        for(int i = 0; i < 2; i++)
        {
            for(int j = 0; j < 2; j++)
            {
                currentDisplacementArray[simulationResolutions.x * (simulationResolutions.y / 2 - i) + simulationResolutions.x / 2 - j] = .2f;
            }
        }
        _currentDisplacement.SetData(currentDisplacementArray);
        _pastDisplacement.SetData(pastDisplacementArray);
        currentDisplacementArray = null;
        pastDisplacementArray = null;
    }

    //毎フレーム実行するシミュレーション本体
    void Simulate()
    {
        ComputeShader cs = WaveCS;
        int id = -1;

        //スレッドグループの数を求める
        int threadGroupSize = Mathf.CeilToInt(simulationResolution / SIMULATION_BLOCK_SIZE);

        //各パラメータをComputeShaderにセット
        id = cs.FindKernel("Compute");
        cs.SetInt("_Res", simulationResolution);
        cs.SetInt("_ResX", simulationResolutions.x);
        cs.SetInt("_ResY", simulationResolutions.y);
        cs.SetFloat("_StepX", positionStep.x);
        cs.SetFloat("_StepY", positionStep.y);
        cs.SetFloat("_StepT", timeStep);
        cs.SetFloat("_Velocity", velocity);
        cs.SetBuffer(id, "_CurrentDispBuffer", _currentDisplacement);
        cs.SetBuffer(id, "_PastDispBuffer", _pastDisplacement);
        //ComputeShaderを実行
        cs.Dispatch(id, threadGroupSize, 1, 1);
    }

    //ComputeShaderを明示的に破棄
    void ReleaseBuffer()
    {
        if(_currentDisplacement != null)
        {
            _currentDisplacement.Release();
            _currentDisplacement = null;
        }

        if(_pastDisplacement != null)
        {
            _pastDisplacement.Release();
            _pastDisplacement = null;
        }
    }
}

細かい説明は Unity Graphics Programming などに譲るとして、流れとしては、

  1. シミュレーションの解像度、空間・時間のステップ幅(δx,δy,δt)、波速(c)などのパラメーターを設定する。
  2. シミュレーションするブロックの数だけの長さの ComputeBuffer を用意し、初期値を入れてあげる。
  3. 使うパラメーターと一緒に ComputeShader に渡す。

といった感じになっています。
ComputeBuffer については、明示的に破棄する必要があるので、気を付けて下さい。

ComputeWave.compute

//カーネル関数を指定
#pragma kernel Compute

//スレッドグループのスレッドサイズ
#define SIMULATION_BLOCK_SIZE 256

//変位のバッファ
RWStructuredBuffer<float> _CurrentDispBuffer;
RWStructuredBuffer<float> _PastDispBuffer;

int _Res;
int _ResX;
int _ResY;
float _StepX;
float _StepY;
float _StepT;
float _Velocity;

[numthreads(SIMULATION_BLOCK_SIZE, 1, 1)]
void Compute
(
    // スレッド全体で固有のID
    uint3 DTid : SV_DispatchThreadID
)
{
    int index = DTid.x;
    //1次元の連番をx, yに直す
    int x = index % _ResX;
    int y = index / _ResX;
    //件の波動方程式の計算
    float result = _Velocity * _Velocity * _StepT * _StepT 
        * ((_CurrentDispBuffer[clamp(x+1, 0, _ResX-1) + y * _ResX] - 2 * _CurrentDispBuffer[index] + _CurrentDispBuffer[clamp(x-1, 0, _ResX-1) + y * _ResX]) / (_StepX * _StepX) 
        + (_CurrentDispBuffer[x + clamp(y+1, 0, _ResY-1) * _ResX] - 2 * _CurrentDispBuffer[index] + _CurrentDispBuffer[x + clamp(y-1, 0, _ResY-1) * _ResX]) / (_StepY * _StepY)) 
        - (_PastDispBuffer[index] - 2 * _CurrentDispBuffer[index]);
    //バッファの更新
    _PastDispBuffer[index] = _CurrentDispBuffer[index];
    _CurrentDispBuffer[index] = result;
}

ここでは実際に、離散化した波動方程式の計算をマスごとに行っています。

RenderWave.cs

using UnityEngine;

[RequireComponent(typeof(ComputeWave))]
public class RenderWave : MonoBehaviour
{
    //キューブの大きさ
    Vector3 objectScale;

    //シミュレーション結果を持ってるComputeWaveスクリプト
    [SerializeField] ComputeWave computeWave;

    //表示するメッシュ(キューブ)
    [SerializeField] Mesh instanceMesh;
    //表示に使うマテリアル
    [SerializeField] Material instanceRenderMaterial;

    //GPUInstancingのための引数
    uint[] args = new uint[5] { 0, 0, 0, 0, 0 };
    ComputeBuffer argsBuffer;

    void Start()
    {
        //表示するキューブの大きさを指定
        objectScale.x = computeWave.positionStep.x;
        objectScale.y = computeWave.positionStep.y;
        objectScale.z = computeWave.positionStep.y;

        argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments); 
    }

    void FixedUpdate()
    {
        RenderInstancedMesh();
    }

    private void OnDisable()
    {
        if (argsBuffer != null)
            argsBuffer.Release();
        argsBuffer = null;
    }

    //GPUInstancingのメソッド
    void RenderInstancedMesh()
    {
        if (instanceRenderMaterial == null || computeWave == null || !SystemInfo.supportsInstancing)
            return;


        uint numIndices = (instanceMesh != null) ? (uint)instanceMesh.GetIndexCount(0) : 0;
        args[0] = numIndices;
        args[1] = (uint)computeWave.simulationResolution;
        argsBuffer.SetData(args);

        instanceRenderMaterial.SetBuffer("_WaveBuffer", computeWave._currentDisplacement);
        instanceRenderMaterial.SetVector("_ObjectScale", objectScale);
        instanceRenderMaterial.SetInt("_ResolutionX", computeWave.simulationResolutions.x);
        instanceRenderMaterial.SetInt("_ResolutionZ", computeWave.simulationResolutions.y);
        instanceRenderMaterial.SetFloat("_StepX", computeWave.positionStep.x);
        instanceRenderMaterial.SetFloat("_StepZ", computeWave.positionStep.y);

        var bounds = new Bounds
        (
            computeWave.wallCenter,
            computeWave.wallSize
        );

        Graphics.DrawMeshInstancedIndirect
        (
            instanceMesh,
            0,
            instanceRenderMaterial,
            bounds,
            argsBuffer
        );
    }
}

ComputeWave.computeで計算した結果である、ComputeWave.cs_currentDisplacementを RenderWave.shaderに渡して、GPU Instancing をしています。

RenderWave.shader

Shader "Custom/RenderWave"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness("Smoothness", Range(0,1)) = 0.5
        _Metallic("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Standard vertex:vert addshadow
        #pragma instancing_options procedural:setup

        struct Input
        {
            float2 uv_MainTex;
        };

        #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
        // 変位の構造体バッファ
        StructuredBuffer<float> _WaveBuffer;
        #endif

        sampler2D _MainTex;
        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        float3 _ObjectScale;
        int _ResolutionX;
        int _ResolutionZ;
        float _StepX;
        float _StepZ;

        //IDから位置を計算する
        float3 CalcPos(int ID, float d) 
        {
            return float3(
                _StepX * (ID % _ResolutionX - _ResolutionX / 2),
                d,
                _StepZ * (ID / _ResolutionX - _ResolutionZ / 2)
                );
        }

        void vert(inout appdata_full v)
        {
            #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
            //場所ごとの変位を取り出す
            float disp = _WaveBuffer[unity_InstanceID];
            //場所の計算
            float3 pos = CalcPos(unity_InstanceID, disp);
            //スケールの取得
            float3 scl = _ObjectScale;
            // オブジェクト座標からワールド座標に変換する行列を定義
            float4x4 object2world = (float4x4)0;
            // スケール値を代入
            object2world._11_22_33_44 = float4(scl.xyz, 1.0);
            // 行列に位置(平行移動)を適用
            object2world._14_24_34 += pos.xyz;
            // 頂点を座標変換
            v.vertex = mul(object2world, v.vertex);
            // 法線を座標変換
            v.normal = normalize(mul(object2world, v.normal));
            #endif
        }

        void setup()
        {
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

最終的に、ここにで計算結果を表示してあげます。シミュレーションの空間ステップから表示するキューブのscaleを求めて、変位とインデックスからpositionを計算して、ワールド座標に変換する行列を定義してあげます。それで各オブジェクトを変形してあげれば波の可視化ができます。

エディタ上での設定

image.png
最後に、以上のスクリプトを上のように適当なオブジェクトにアタッチします。
この時、RenderWave.shaderを付けたマテリアルのEnable GPU Instancingをonにするのを忘れないようにします。


はい、完成です。
これが見たかったんです!

今後の課題

image.png
とはいえこいつ、油断すると発散するんですよね…。今度はちゃんとエネルギーの保存とかにも気を付けられるようにしたいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tilemapのアップデート 2019

昨年のUnityアドベントカレンダーにて

さぁ、UnityのTilemapを始めよう!

というシリーズを書きました。Tilemapを使ってみたい!という人は、そちらを読んでみてください!

Unity 2017.2でリリースされたTilemapはその後、

  • Unity 2018.2でHexagonal Tile
  • Unity 2018.3でIsometric Tile

が追加されました。

それ以外にもUnity 2019.2から内部的な重要な変更が行われ、いくつかの細かい改善が行われました。

本投稿ではUnity 2019.2時点でのTilemapの内部的な変更と細かい改善を紹介します。

Tilemap EditorはUnity Package形式に分離

Unity 2019.2からTilemap Editorの機能は、「2D Tilemap Editor」というUnity Packageに切り出されました。そのため、Unity 2019.2以降でTilemapを使うためにはUnity Package Managerで「2D Tilemap Editor」パッケージへの依存が設定されている必要があります。

  • Unity 2019.2.14以降で2Dプロジェクトとして初期化したプロジェクトは、プロジェクト作成段階で2D Tilemap Editorへの依存が設定されています。
  • Unity 2019.2.0から2019.2.13までで初期化したプロジェクトは、プロジェクト作成段階で2D Tilemap Editorへの依存が設定されていないので、自分で設定する必要があります。
  • Unity 2019.2.0以降で3Dプロジェクトととして初期化したプロジェクトは、プロジェクト作成段階で2D Tilemap Editorへの依存が設定されていないので、自分で設定する必要があります。

自分で「2D Tilemap Editor」への依存を設定する場合は、Window > Package Manager からPackage Manager Windowを開き、「2D Tilemap Editor」をインストールしてください。

tilemap.png

また同じように、Tilemapで1枚の画像を複数のSpriteに分割するために使うSprite Editor Windowを使うために、「2D Sprite」というパッケージが必要になりました。

もし、Tilemapに関するUIが出てこないのであれば、「2D Tilemap Editor」への依存が設定されているのかを確認してください。

Unity 2019.2.14(本投稿執筆時の最新)で仕様が変わりました。Unity 2019.2.0などでプロジェクトを初期化した人は注意してください。

その他の細かい変更

その他、Tilemapにまつわる細かい変更を紹介します。

2d-extrasもPackage Manager経由で入れらえるように

2d-extrasは、TilemapのUnity公式拡張ライブラリです。Terrain TilePrefab Brushなど便利なカスタムタイル・カスタムブラシを提供しています。

今まではプロジェクトにソースコードをコピーするしか導入方法がありませんでしたが、Unity 2019.2以降であれば、Package Manager経由でのインストールも可能となりました。

プロジェクトルート/Packages/package.jsondependenciesに、次の一行を加えてください。

"com.unity.2d.tilemap.extras": "https://github.com/Unity-Technologies/2d-extras.git#v1.3.1"

v1.3.1の代わりにインストールしたい2d extrasのバージョンを指定してください。執筆時の最新バージョンは、v1.3.1です。

将来的には、GitHub経由でなく、公式Unity Package Managerから導入できるかもしれません。

関連 : https://github.com/Unity-Technologies/2d-extras/issues/152#issuecomment-552774057

-と=というz座標を変える新たなショートカットが追加

「Isometric Z as Y」でTilemapのステージを構築する際、頻繁にz座標を編集します。

この操作のために、Unity 2018.3からTilemapを編集する際に新たなショートカットが加わりました。

Windows : ;(減少)と-(増加)
macOS : =(減少)と-(増加)

関連記事 : Tilemap Editorショートカットキーのメモ

2018.3.0のリリースノートより

z座標を変えられるかのトグルが追加

Unity 2019.2からz座標を変えられるかのトグルが追加されました。

tilemap_z_enable.png

2019.2.0のリリースノートより

Grid + TilemapのプレハブをTilePaletteに変換できるように

Unity 2019.2.0から、Grid + TilemapのプレハブをTilePaletteウィンドウで変換できるようになりました。

  • Grid + Tilemapのプレハブを作成する
  • TilePaletteウィンドウにドラッグアンドドロップする
  • Tile Paletteになる

2019.2.0のリリースノートより

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む