見出し画像

Cesiumでジオラマアプリを作ってみよう! 第4回「カメラ編」(全4回)

はじめに

最後の第4回「カメラ編」ではカメラの視点を切り替えて、バードビューとウォークビューを交互に切り替えるようにしたいと思います。ウォークビューは街並みを歩く感じになるため、また違った楽しみ方があると思います。

  • バードビュー : 過去3回までやってきた上から全体を見上げるような俯瞰視点

  • ウォークビュー : 等身大の街並みを見るような歩く時の視点

★クリックして動画を再生★

第4回「カメラ編」目次


ウォークビュー

今回はバードビュー時にRaycastした部分を降下地点とするため、その座標をカメラに設定します。
第2回「座標編」でも紹介したGlobeAnchorを使うことにより緯度経度も同期することができるので、
CesiumGeoreferenceのScale値が大きくなったとしても、その座標を維持することができます。
CesiumGeoreferenceのScale値には元々の値である”1″を設定することで等身大の大きさにすることができます。後は、バードビュー時に行っている円形のクリッピングが不要なのでこれを切ります。

/// <summary>
/// ウォークビュー起動
/// </summary>
/// <param name="dropPoint">降下地点</param>
public async UniTask RunWalkViewAsync(Vector3 dropPoint)
{
    await _screenCanvas.FadeInAsync();
    _cameraController.SyncAnchor(dropPoint);
    _cesiumGeoreference.scale = SCALE_WALK_VIEW;
    _cameraController.ReverseSync();
    setClipping(false);
    _viewMode = Mode.Walk;
    _screenCanvas.EnableSelectingMode(false);
    _screenCanvas.SetViewChangeText("バードビューに切り替え");
    await _screenCanvas.FadeOutAsync();
}

/// <summary>
/// クリッピングの設定
/// </summary>
/// <param name="enable"></param>
private void setClipping(bool enable)
{
    Shader.SetGlobalFloat(_isClippingId,enable ? 1f : 0f);
    _excluder.enabled = enable;
}

もし、ウォークビュー切り替え時に読み込み過程が気になるようであれば、Cesium3DTilesetが持つComputeLoadProgress()で完全に読み込むまで待機して、画面を表示みたいな演出をやると良いと思います。

/// <summary>
/// 3DTileset
/// </summary>
[SerializeField] 
private Cesium3DTileset _tileset;

/// <summary>
/// 3DTilesの読み込み
/// </summary>
/// <param name="ct"></param>
public async UniTask LoadingAsync(CancellationToken ct)
{
    await UniTask.WaitUntil(() => _tileset.ComputeLoadProgress() >= 99.99f, cancellationToken: ct);
}

カメラ操作

ウォークビュー中は土台の概念が無くなるため、カメラそのものを動かすことになります。
Cesiumには”CesiumCameraController”というのが用意されていますが、今回は使用せずにアンカーを別途用意して自前で動かします。Transformをそのまま動かしたら良いので特に難しいことはないです。
カメラを動かしたらそのたびにアンカーも同期してください。
もしQuest等でXROriginを使用するなら、MoveCameraToWorldLocation()RotateAroundCameraPosition()を使用して動かすことを推奨します。
以下のスクリプトをカメラにそのままアタッチしたら良いと思います。

/// <summary>
/// カメラの操作
/// </summary>
public class CameraController : MonoBehaviour
{
    /// <summary>
    /// カメラと同期用のアンカー
    /// </summary>
    [SerializeField]
    private CesiumGlobeAnchor _anchor;

    /// <summary>
    /// カメラの初期姿勢
    /// </summary>
    private Pose _initPose;
    
    /// <summary>
    /// 基準の早さ
    /// </summary>
    private const float BASE_SPEED = 0.25f;

    private void Start()
    {
        _initPose = new Pose(transform.position, transform.rotation);
    }

    /// <summary>
    /// アンカーの同期(Unity座標)
    /// </summary>
    /// <param name="position"></param>
    public void SyncAnchor(Vector3 position)
    {
        _anchor.transform.position = position;
        _anchor.Sync();
    }
    
    /// <summary>
    /// アンカーの同期(緯度経度)
    /// </summary>
    public void SyncAnchor(double latitude,double longitude)
    {
        //高度は仮
        double3 longitudeLatitudeHeight = new double3(longitude,latitude,200);
        _anchor.longitudeLatitudeHeight = longitudeLatitudeHeight;
        _anchor.Sync();
        //高さは自身に垂直にRayCastして取得
        if (RayCastUtility.VerticallyCast(_anchor.transform, out Vector3 hitPoint))
        {
            _anchor.transform.position = hitPoint;
            _anchor.Sync();
        }
    }

    /// <summary>
    /// アンカーの座標からカメラ座標を同期する
    /// </summary>
    public void ReverseSync()
    {
        transform.position = _anchor.transform.position;
    }

    /// <summary>
    /// カメラの緯度経度座標の取得
    /// </summary>
    /// <returns></returns>
    public double3 GetGeoPosition()
    {
        return _anchor.longitudeLatitudeHeight;
    }

    /// <summary>
    /// カメラのリセット
    /// </summary>
    public void ResetPosition()
    {
        transform.SetPositionAndRotation(_initPose.position,_initPose.rotation);
    }
    
    /// <summary>
    /// カメラの水平移動
    /// </summary>
    /// <param name="vector"></param>
    public void Move(Vector2 vector)
    {
        var selfT = transform;
        var horizontalAxis = selfT.right * vector.x * BASE_SPEED;
        var verticalAxis = selfT.forward * vector.y * BASE_SPEED;
        horizontalAxis.y = 0;
        verticalAxis.y = 0;
        selfT.position += horizontalAxis + verticalAxis;
        SyncAnchor(selfT.position);
    }
    
    /// <summary>
    /// カメラの高さ移動
    /// </summary>
    /// <param name="vector"></param>
    public void MoveHeight(float vector)
    {
        var selfT = transform;
        selfT.position += Vector3.up * vector * BASE_SPEED;
        SyncAnchor(selfT.position);
    }

    /// <summary>
    /// カメラの回転
    /// </summary>
    /// <param name="angle"></param>
    public void Rotate(float angle)
    {
        transform.Rotate(0f,angle,0f);
    }
    
}//CameraController End

音声入力で移動する時

第3回「API連携編」でやった時は、土台でもあるCesiumGeoreferenceを動かしていました。
ウォークビュー時はカメラを動かすため、カメラのアンカーに緯度経度を設定した後に、カメラの座標を同期します。
以下のスクリプトは第3回のVoiceGeocoderクラスに、ウォークビュー時を追加しています。

/// <summary>
/// 音声入力で地理座標の移動
/// </summary>
private async UniTaskVoid geocodeAsync()
{
    const int resultCount = 1;
    const int errorTime = 3;
    
    try
    {
        screenCanvas.EnableWaiting(true);
        //音声認識開始
        var recognizeStr = await _winVoiceRecognizer.StartReconizeAsync(_ct);
        screenCanvas.SetMicText($"認識結果:{recognizeStr}");
        _winVoiceRecognizer.StopReconize();
        _openAiService.InitChat();
        //認識結果をOpenAIに渡して主語の取得
        var responseKeyword = await _openAiService.RequestKeywordAsync(recognizeStr, Model.Four, _ct);
        var targetStr = responseKeyword.keyword;
        screenCanvas.SetMicText($"移動先:{targetStr}");
        _openAiService.ClearChatMemory();
        //Googleに主語を渡して緯度経度の取得
        var responseBody = await GoogleAPI.RequestTextSearchAsync(targetStr, resultCount, cancellationToken: _ct);
        var location = responseBody.places[0].location;
        switch (_viewManager.ViewMode)
        {
            case ViewManager.Mode.Bird:
                _cesiumGeoreference.latitude = location.latitude;
                _cesiumGeoreference.longitude = location.longitude;
                break;
            case ViewManager.Mode.Walk:
                _cameraController.SyncAnchor(location.latitude,location.longitude);
                _cameraController.ReverseSync();
                break;
        }
    }
    catch (TimeoutException e)
    {
        screenCanvas.SetMicText("音声認識タイムアウト");
        await UniTask.Delay(TimeSpan.FromSeconds(errorTime),cancellationToken:_ct);
    }
    catch (Exception e)
    {
        screenCanvas.SetMicText($"エラー:{e.Message}");
        await UniTask.Delay(TimeSpan.FromSeconds(errorTime),cancellationToken:_ct);
    }
    finally
    {
        screenCanvas.EnableWaiting(false);   
    }
}

その他に必要な設定集

ウォークビューだと世界のサイズ感が大きくなるため、以下のような各種設定が必要です。

・カメラのFarClip : 数値を大きくしないと遠くが見えないです

・Raycastの最大距離 : 数値を大きくしないと判定が取れないです

・Pinの大きさ : Scaleを大きくしないと小さくて見えないです

バードビュー

逆にウォークビューから元に戻す作業になります。
今回は、バードビューに戻る前にカメラがいた場所を表示させたいため、
カメラのアンカーから緯度経度を取得して、CesiumGeoreferenceに反映させています。
後は、カメラの座標とCesiumGeoreferenceのScale値を元に戻し、円形のクリッピングを再設定しています。

/// <summary>
/// バードビュー起動
/// </summary>
public async UniTask RunBirdViewAsync()
{
    await _screenCanvas.FadeInAsync();
    var cameraGeoPosition = _cameraController.GetGeoPosition();
    _cesiumGeoreference.longitude = cameraGeoPosition.x;
    _cesiumGeoreference.latitude = cameraGeoPosition.y;
    _cesiumGeoreference.scale = SCALE_BIRD_VIEW;
    _cameraController.ResetPosition();
    setClipping(true);
    _viewMode= Mode.Bird;
    _screenCanvas.SetViewChangeText("ウォークビューに切り替え");
    await _screenCanvas.FadeOutAsync();
}

スクリプト全容

BaseTilesetController

第2回~第4回までの内容を含んだ土台操作に関わるクラスです。

/// <summary>
/// 土台を操作する
/// </summary>
[RequireComponent(typeof(CesiumGeoreference))]
public class BaseTilesetController : MonoBehaviour
{
    #region Variables
    
    /// <summary>
    /// 移動感度
    /// </summary>
    [SerializeField]
    private float _moveSensitivity = 0.05f;

    /// <summary>
    /// Googleの3DTiles
    /// </summary>
    [SerializeField] 
    private Cesium3DTileset _googleTileset;
    
    /// <summary>
    /// Rayの座標変換用アンカー
    /// </summary>
    [SerializeField] 
    private CesiumGlobeAnchor _rayAnchor;
    
    /// <summary>
    /// ピンの親トランスフォーム
    /// </summary>
    [SerializeField] 
    private Transform _pinParentT;

    /// <summary>
    /// バードビュー用のPinのPrefab
    /// </summary>
    [SerializeField] 
    private GameObject _birdPinPrefab;

    /// <summary>
    /// ウォークビュー用のPinPrefab
    /// </summary>
    [SerializeField] 
    private GameObject _walkPinPrefab;
    
    /// <summary>
    /// Viewの視点切り替え管理
    /// </summary>
    [SerializeField] 
    private ViewManager _viewManager;
    
    /// <summary>
    /// CesiumGeoreference
    /// </summary>
    private CesiumGeoreference _cesiumGeoreference;
    
    /// <summary>
    /// Tilesetの親トランスフォーム
    /// </summary>
    private Transform _tilesetParentT;
    
    /// <summary>
    /// キャンセルトークン
    /// </summary>
    private CancellationToken _ct;
    
    /// <summary>
    /// PivotプロパティのID
    /// </summary>
    private readonly int _pivotPropartyId = Shader.PropertyToID("_Pivot");
    
    #endregion
    
    #region UnityLifeCycle
    void Start()
    {
        //3DTilesが生成された時のコールバック登録
        _googleTileset.OnTileGameObjectCreated += onTileGameObjectCreated;
        
        _ct = destroyCancellationToken;
        
        _cesiumGeoreference = GetComponent<CesiumGeoreference>();
        _tilesetParentT = transform.GetChild(0);
        updateClippingPivot();
    }

    private void OnDestroy()
    {
        //3DTilesが生成された時のコールバック解除
        _googleTileset.OnTileGameObjectCreated -= onTileGameObjectCreated;
    }

    #endregion
    
    #region PublicMethod
    /// <summary>
    /// 土台の水平移動
    /// </summary>
    /// <param name="vector">移動方向</param>
    public void MoveTileset(Vector2 vector)
    {
        var moveVector = getDestinationPosition(vector);
        _tilesetParentT.position += moveVector;
        updateClippingPivot();
    }
    
    /// <summary>
    /// 土台の高さの移動
    /// </summary>
    /// <param name="value">高さ</param>
    public void MoveHeightTileset(float value)
    {
        value *= _moveSensitivity;
        var posBuff = _tilesetParentT.position;
        posBuff.y += value;
        _tilesetParentT.position = posBuff;
    }

    /// <summary>
    /// 土台の回転
    /// </summary>
    /// <param name="angle">角度</param>
    public void RotateTileset(float angle)
    {
        _tilesetParentT.Rotate(0f,angle,0f);
    }
    
    /// <summary>
    /// 地理座標の移動
    /// </summary>
    /// <param name="vector">移動方向</param>
    public void MoveGeographicCoordinates(Vector2 vector)
    {
        var moveVector = getDestinationPosition(vector);

        //土台の回転分、ベクトルを回転させる
        var angle = _tilesetParentT.rotation.eulerAngles.y;
        if (angle != 0) angle = 360f - angle;
        var rotation = Quaternion.AngleAxis(angle, Vector3.up);
        var rotateVector = rotation * moveVector;
        double3 dMoveVector = new double3(rotateVector);

        //Unity座標系から地球座標系の方向に変換
        var unityDirection = _cesiumGeoreference.TransformUnityDirectionToEarthCenteredEarthFixed(dMoveVector);
        _cesiumGeoreference.ecefX += unityDirection.x;
        _cesiumGeoreference.ecefY += unityDirection.y;
        _cesiumGeoreference.ecefZ += unityDirection.z;
    }
    #endregion
    
    #region PrivateMethod
    /// <summary>
    /// 移動予定先の座標の取得
    /// </summary>
    /// <param name="vector">移動方向</param>
    /// <returns></returns>
    private Vector3 getDestinationPosition(Vector2 vector)
    {
        var cameraT = Camera.main.transform;
        var sensitivity = _moveSensitivity * -1f;
        var vertical = cameraT.forward * vector.y * sensitivity;
        var horizontal = cameraT.right * vector.x * sensitivity;
        var moveVector = vertical + horizontal;
        //高さは無視
        moveVector.y = 0f;
        return moveVector;
    }
    
    /// <summary>
    /// 描画の中心の更新
    /// </summary>
    private void updateClippingPivot()
    {
        Vector3 tileSetPos = _tilesetParentT.transform.position;
        Vector2 pivot = new Vector2(tileSetPos.x,tileSetPos.z);
        Shader.SetGlobalVector(_pivotPropartyId,pivot);
    }
    
    /// <summary>
    /// Rayが当たった時
    /// </summary>
    /// <param name="hitPosition"></param>
    private void onRayCastTiles(Vector3 hitPosition)
    {
        _rayAnchor.transform.position = hitPosition;
        _rayAnchor.Sync();
        var latitude = _rayAnchor.longitudeLatitudeHeight.y;
        var longitude = _rayAnchor.longitudeLatitudeHeight.x;
        Debug.Log($"Hit: 座標:{hitPosition} : 緯度:{latitude},経度:{longitude}");

        if (_viewManager.ViewMode == ViewManager.Mode.Selecting)
        {
            _viewManager.RunWalkViewAsync(hitPosition).Forget();
        }
        else
        {
            //Rayの当たった場所にPinを生成
            var pinPrefab = _viewManager.ViewMode == ViewManager.Mode.Bird ? _birdPinPrefab : _walkPinPrefab;
            var pinObj = Instantiate(pinPrefab,hitPosition,Quaternion.identity,_pinParentT);
            var pinRenderer = pinObj.GetComponent<PinRenderer>();
            if (_viewManager.ViewMode == ViewManager.Mode.Walk)
            {
                //ウォークビュー時は描画範囲のチェックを行わない
                pinRenderer.SetRenderingCheck(false);
            }
            //Pinに付近の施設情報の表示
            nearbyInfoAsync(pinRenderer).Forget();    
        }
    }

    /// <summary>
    /// 3DTilesオブジェクトが作成された時のコールバック
    /// </summary>
    /// <param name="tileObj"></param>
    private void onTileGameObjectCreated(GameObject tileObj)
    {
        //コントローラーのRayがHit時のコールバック登録
        var interactable = tileObj.AddComponent<CesiumRayInteractable>();
        interactable.onSelected = onRayCastTiles;
    }

    /// <summary>
    /// ピンに周辺情報の表示
    /// </summary>
    /// <param name="pin"></param>
    private async UniTask nearbyInfoAsync(PinRenderer pin)
    {
        const float radius = 100f;
        const int resultCount = 10;
        var longitudeLatitudeHeight = pin.SyncGeographicPosition();
        double2 geoPos = new double2(longitudeLatitudeHeight.x, longitudeLatitudeHeight.y);
        string errorText = null;
        var resultList = new List<Place>();
        try
        {
            var response = await GoogleAPI.RequestNearBySearchAsync(geoPos.y,geoPos.x, resultCount,radius, _ct);
            resultList = response.places;
        }
        catch (Exception e)
        {
            errorText = $"APIエラー : {e}";
            Debug.Log(errorText);
            pin.SetErrorText(errorText,Color.red);
            return;
        }
        if (resultList.Count == 0)
        {
            pin.SetErrorText("周辺情報がありませんでした",Color.yellow);
            return;
        }
        foreach (var result in resultList)
        {
            if (!string.IsNullOrEmpty(result.displayName.text))
            {
                //ピンの中のセルの作成
                var cell = pin.CreateInfoCell(result);
                if (result.photos.Count > 0)
                {
                    var photoName = result.photos[0].name;
                    loadCellThumbnailAsync(cell,photoName).Forget();
                }
            }
        }
    }

    /// <summary>
    /// セルのサムネイル画像を読み込んで表示
    /// </summary>
    /// <param name="cell"></param>
    /// <param name="photoName"></param>
    private async UniTaskVoid loadCellThumbnailAsync(SearchDataCell cell,string photoName)
    {
        const int maxHeightPx = 256;
        const int maxWidthPx  = 144;
        var response = await GoogleAPI.RequestPlacePhoto(photoName, maxHeightPx, maxWidthPx, _ct);
        var imageUrl = response.photoUri;
        if (!string.IsNullOrEmpty(imageUrl))
        {
            using var request = UnityWebRequestTexture.GetTexture(imageUrl);
            await request.SendWebRequest().WithCancellation(_ct);
            if (request.result != UnityWebRequest.Result.Success)
            {
                Debug.Log($"サムネイル取得失敗:{request.error}");
                return;
            }
            var texture = DownloadHandlerTexture.GetContent(request);
            cell.SetThumbnail(texture);
        }
    }
    
    #endregion
    
}//BaseTilesetController End

KeyboardInteractor

キーボード入力による操作を行っているクラスです。
第2回「座標編」にて、BaseTilesetController内で実装されていた土台の移動はこちらに移しています。

/// <summary>
/// キーボード入力による操作
/// </summary>
public class KeyboardInteractor : MonoBehaviour
{
    /// <summary>
    /// Tileset操作
    /// </summary>
    [SerializeField] 
    private BaseTilesetController _baseTilesetController;
    
    /// <summary>
    /// カメラ操作
    /// </summary>
    [SerializeField]
    private CameraController _cameraController;
    
    /// <summary>
    /// ビュー管理
    /// </summary>
    [SerializeField]
    private ViewManager _viewManager;

    void Update()
    {
        switch (_viewManager.ViewMode)
        {
            case ViewManager.Mode.Bird:
                birdViewControll();
                break;
            case ViewManager.Mode.Walk:
                walkViewControll();
                break;
        }
    }

    /// <summary>
    /// バードビュー時の操作
    /// </summary>
    private void birdViewControll()
    {
        //Space押下中は緯度経度の移動
        bool isGeoMove = Input.GetKey(KeyCode.Space);
        
        //--- WASDキー(水平移動、緯度経度移動) ---//
        if (Input.GetKey(KeyCode.W))
        {
            //経度
            if(isGeoMove) _baseTilesetController.MoveGeographicCoordinates(new Vector2(0f, 1f));
            else _baseTilesetController.MoveTileset(new Vector2(0f, -1f));
        }
        if (Input.GetKey(KeyCode.S))
        {
            //経度
            if(isGeoMove) _baseTilesetController.MoveGeographicCoordinates(new Vector2(0f, -1f));
            else _baseTilesetController.MoveTileset(new Vector2(0f, 1f));
        }
        if (Input.GetKey(KeyCode.A))
        {
            //緯度
            if(isGeoMove) _baseTilesetController.MoveGeographicCoordinates(new Vector2(-1f, 0f));
            else _baseTilesetController.MoveTileset(new Vector2(1f, 0f));
        }
        if (Input.GetKey(KeyCode.D))
        {
            //緯度
            if(isGeoMove) _baseTilesetController.MoveGeographicCoordinates(new Vector2(1f, 0f));
            else _baseTilesetController.MoveTileset(new Vector2(-1f, 0f));
        }
        
        //--- 上下左右キー(高さ、回転) ---//
        if (Input.GetKey(KeyCode.UpArrow))
        {
            _baseTilesetController.MoveHeightTileset(1f);
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            _baseTilesetController.MoveHeightTileset(-1f);
        }
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            _baseTilesetController.RotateTileset(-0.1f);
        }
        if (Input.GetKey(KeyCode.RightArrow))
        {
            _baseTilesetController.RotateTileset(0.1f);
        }
    }
    
    /// <summary>
    /// ウォークビュー時の操作
    /// </summary>
    private void walkViewControll()
    {
        //--- WASDキー(水平移動) ---//
        if (Input.GetKey(KeyCode.W))
        {
            _cameraController.Move(new Vector2(0f,1f));
        }
        if (Input.GetKey(KeyCode.S))
        {
            _cameraController.Move(new Vector2(0f,-1f));
        }
        if (Input.GetKey(KeyCode.A))
        {
            _cameraController.Move(new Vector2(-1f,0f));
        }
        if (Input.GetKey(KeyCode.D))
        {
            _cameraController.Move(new Vector2(1f,0f));
        }
        
        //--- 上下左右キー(高さ、回転) ---//
        if (Input.GetKey(KeyCode.UpArrow))
        {
            _cameraController.MoveHeight(1f);
        }
        if (Input.GetKey(KeyCode.DownArrow))
        {
            _cameraController.MoveHeight(-1f);
        }
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            _cameraController.Rotate(-0.5f);
        }

        if (Input.GetKey(KeyCode.RightArrow))
        {
            _cameraController.Rotate(0.5f);
        }
    }
    
}//InputController End

ViewManager

ウォークビューとバードビューの切り替えを管理しているクラスです。

/// <summary>
/// 視点切り替えの管理
/// </summary>
[RequireComponent(typeof(CesiumGeoreference))]
public class ViewManager : MonoBehaviour
{
    /// <summary>
    /// ビューの状態
    /// </summary>
    public enum Mode
    {
        /// <summary>
        /// バードビュー
        /// </summary>
        Bird,
        
        /// <summary>
        /// ウォークビュー
        /// </summary>
        Walk,
        
        /// <summary>
        /// 降下地点選択中
        /// </summary>
        Selecting,
    }
    
    /// <summary>
    /// カメラ操作
    /// </summary>
    [SerializeField]
    private CameraController _cameraController;
    
    /// <summary>
    /// Cesiumの矩形クリップ
    /// </summary>
    [SerializeField] 
    private CesiumBoxExcluder _excluder;
    
    /// <summary>
    /// スクリーンのUI
    /// </summary>
    [SerializeField]
    private ScreenCanvas _screenCanvas;
    
    /// <summary>
    /// CesiumGeoreference
    /// </summary>
    private CesiumGeoreference _cesiumGeoreference;
    
    /// <summary>
    /// ビューの状態
    /// </summary>
    private Mode _viewMode = Mode.Bird;
    
    /// <summary>
    /// ウォークビューのスケール
    /// </summary>
    private const float SCALE_WALK_VIEW = 1f;
    
    /// <summary>
    /// バードビューのスケール
    /// </summary>
    private const float SCALE_BIRD_VIEW = 0.0025f;
    
    /// <summary>
    /// IsTilesClippingプロパティのID
    /// </summary>
    private readonly int _isClippingId = Shader.PropertyToID("_IsTilesClipping");
    
    void Start()
    {
        _cesiumGeoreference = GetComponent<CesiumGeoreference>();
        setClipping(true);
        _screenCanvas.OnClickViewChangeObservable
                     .Subscribe(_ =>
                     {
                         switch (ViewMode)
                         {
                             case Mode.Bird:
                                 _viewMode = Mode.Selecting;
                                 _screenCanvas.EnableSelectingMode(true);
                                 break;
                             case Mode.Walk:
                                 RunBirdViewAsync().Forget();
                                 break;
                         }
                     })
                     .AddTo(this);
    }

    /// <summary>
    /// ビューの状態
    /// </summary>
    public Mode ViewMode => _viewMode;
    
    /// <summary>
    /// バードビュー起動
    /// </summary>
    public async UniTask RunBirdViewAsync()
    {
        await _screenCanvas.FadeInAsync();
        var cameraGeoPosition = _cameraController.GetGeoPosition();
        _cesiumGeoreference.longitude = cameraGeoPosition.x;
        _cesiumGeoreference.latitude = cameraGeoPosition.y;
        _cesiumGeoreference.scale = SCALE_BIRD_VIEW;
        _cameraController.ResetPosition();
        setClipping(true);
        _viewMode= Mode.Bird;
        _screenCanvas.SetViewChangeText("ウォークビューに切り替え");
        await _screenCanvas.FadeOutAsync();
    }

    /// <summary>
    /// ウォークビュー起動
    /// </summary>
    /// <param name="dropPoint">降下地点</param>
    public async UniTask RunWalkViewAsync(Vector3 dropPoint)
    {
        await _screenCanvas.FadeInAsync();
        _cameraController.SyncAnchor(dropPoint);
        _cesiumGeoreference.scale = SCALE_WALK_VIEW;
        _cameraController.ReverseSync();
        setClipping(false);
        _viewMode = Mode.Walk;
        _screenCanvas.EnableSelectingMode(false);
        _screenCanvas.SetViewChangeText("バードビューに切り替え");
        await _screenCanvas.FadeOutAsync();
    }
    
    /// <summary>
    /// クリッピングの設定
    /// </summary>
    /// <param name="enable"></param>
    private void setClipping(bool enable)
    {
        Shader.SetGlobalFloat(_isClippingId,enable ? 1f : 0f);
        _excluder.enabled = enable;
    }
    
}//ViewManager End

まとめ

最後に紹介したウォークビューはいかがでしょうか?
バードビュー用とウォークビュー用で実装が2倍になるなど大変なこともありますが、
等身大の街並みを歩くことで臨場感があると思います。
本連載ではUnityEditor上でやっていますが、環境がある方はQuest等のHMDで試すと、より臨場感がまして素晴らしいものになるので挑戦してみると良いかと思います。

いいなと思ったら応援しよう!