20210815のC#に関する記事は2件です。

【Unity(C#)】ARFoundationのImageTrackingを使って三人称視点の実装

はじめに ARでの空間共有の手法をまとめた記事が意外となかったので書きます。 既にきれいにまとめている方がいたり、サンプルが出回っていたりしたわけじゃないので もっといいやり方はあるかもしれません。 それはそれで各自どこかにまとめてくれると助かります。 バージョン情報 諸々名前 バージョン Unity 2020.3.4f1 ARFoundation 4.0.12 ARCore XR Plugin 4.0.12 ARKit XR Plugin 4.0.12 XR Plugin Management 4.0.1 PUN 2 2.34.1 デモ 他の人がプレイ中のARが確認可能です。 位置合わせの手法 まず、ARでの位置合わせが一筋縄ではいかない理由を説明します。 2人のプレイヤーがARで空間共有を行う想定の図が下記です。 Unityの原点座標はARを起動した際に決定されます。 Aにとっての原点はBから見れば全く違う座標になってしまいます。Bからの目線も同様です。 この状態でImageTrackingを行ったものが下記の図です。 読み取る画像マーカーの位置が現実空間上で一致していても、 お互いのUnityの原点が異なる以上はAR空間上では同じ位置にあることになりません。 このように、起動した端末の位置に応じてAR空間が展開されるため、何かしらの解決策が必要となります。 ARFoundationにはちゃんと解決策が用意されていました。 説明の前段階として、まずは下記GIFの挙動が重要となります。 【引用元】:Scaling with ARFoundation マウスで動かしているオブジェクトが動いているのではなく、 周りのPlaneやAR Camera(AR Session Origin)が動くことで相対的に動いて見える仕組みです。 この仕組みを使ってUnityのワールド空間の原点と画像マーカーをぴったり重ね合わせます。 下記のようなイメージです。赤い点がUnityのワールド空間の原点です。 すなわち、端末の起動位置に関係無く画像マーカーの位置を原点として扱えるようになるということです。 コード まず、画像マーカーから原点を定める処理を担うScriptです。 適当なオブジェクトにアタッチ using System.Collections; using UnityEngine; using UnityEngine.XR.ARFoundation; /// <summary> /// 画像マーカーから原点を定める /// </summary> public class OriginDecideFromImageMaker : MonoBehaviour { /// <summary> /// ARTrackedImageManager /// </summary> [SerializeField] private ARTrackedImageManager _imageManager; /// <summary> /// ARSessionOrigin /// </summary> [SerializeField] private ARSessionOrigin _sessionOrigin; /// <summary> /// ワールドの原点として振る舞うオブジェクト /// </summary> private GameObject _worldOrigin; /// <summary> /// コルーチン /// </summary> private Coroutine _coroutine; private void OnEnable() { _worldOrigin = new GameObject("Origin"); _imageManager.trackedImagesChanged += OnTrackedImagesChanged; } private void OnDisable() { _imageManager.trackedImagesChanged -= OnTrackedImagesChanged; } /// <summary> /// 原点を定める /// 今回は画像マーカーの位置が原点となる /// </summary> /// <param name="trackedImage">認識した画像マーカー</param> /// <param name="trackInterval">認識のインターバル</param> /// <returns></returns> private IEnumerator OriginDecide(ARTrackedImage trackedImage,float trackInterval) { yield return new WaitForSeconds(trackInterval); var trackedImageTransform = trackedImage.transform; _worldOrigin.transform.SetPositionAndRotation(Vector3.zero,Quaternion.identity); _sessionOrigin.MakeContentAppearAt(_worldOrigin.transform, trackedImageTransform.position,trackedImageTransform.localRotation); _coroutine = null; } /// <summary> /// ワールド座標を任意の点から見たローカル座標に変換 /// </summary> /// <param name="world">ワールド座標</param> /// <returns></returns> public Vector3 WorldToOriginLocal(Vector3 world) { return _worldOrigin.transform.InverseTransformDirection(world); } /// <summary> /// TrackedImagesChanged時の処理 /// </summary> /// <param name="eventArgs">検出イベントに関する引数</param> private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs) { foreach (var trackedImage in eventArgs.added) { StartCoroutine(OriginDecide(trackedImage,0)); } foreach (var trackedImage in eventArgs.updated) { if(_coroutine == null) _coroutine = StartCoroutine(OriginDecide(trackedImage, 5)); } } } MakeContentAppearAt MakeContentAppearAtが先述の"AR Session Originを動かして原点と画像マーカーの位置を合わせる処理" を実行してくれる関数です。 第二、第三引数で指定した位置・回転を、第一引数に渡したTransformに反映します。 ただし、第一引数のTransformに直接値を反映するわけではなく、AR Session Originの位置・回転を変更することで 相対的に指定位置へ移動したように見えるだけなので要注意です。 OnTrackedImagesChanged 過去に少しまとめてます。 【参考リンク】:ARTrackedImageManager.trackedImagesChanged 位置合わせを画像認識するたびに行うとカクカクするので、認識頻度にインターバルを設けています。 ここからはPhotonの実装です。 下記はPhotonの簡易版ルーム入室コードです。 using Photon.Pun; using Photon.Realtime; using UnityEngine; /// <summary> /// サーバーへ接続 /// </summary> public class ConnectPunServer : MonoBehaviourPunCallbacks { [SerializeField] private GameObject _playerPrefab; void Start() { PhotonNetwork.ConnectUsingSettings(); } public override void OnConnectedToMaster() { PhotonNetwork.JoinOrCreateRoom("TestRoom", new RoomOptions(), TypedLobby.Default); } public override void OnJoinedRoom() { PhotonNetwork.Instantiate(_playerPrefab.name, Vector3.zero, Quaternion.identity); } } 次にお絵描き機能です。 using Photon.Pun; using UnityEngine; public class Paint : MonoBehaviourPun { [SerializeField] private GameObject _inkPrefab; [SerializeField] private Transform _inkParent; /// <summary> /// 原点を定めるコンポーネント /// </summary> private OriginDecideFromImageMaker _originDecideFromImageMaker; private void Start() { _originDecideFromImageMaker = FindObjectOfType<OriginDecideFromImageMaker>(); } private void Update() { if (!photonView.IsMine) return; if (0 < Input.touchCount) { var touch = Input.GetTouch(0); var inputPosition = Input.GetTouch(0).position; var paintPosZ = 0.5f; var tmpTouchPos = new Vector3(inputPosition.x, inputPosition.y, paintPosZ); var touchWorldPos = _originDecideFromImageMaker.WorldToOriginLocal(Camera.main.ScreenToWorldPoint(tmpTouchPos)); if (touch.phase == TouchPhase.Began) { photonView.RPC(nameof(PaintStartRPC), RpcTarget.All, touchWorldPos); } else if (touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary) { photonView.RPC(nameof(PaintingRPC), RpcTarget.All, touchWorldPos); } } } /// <summary> /// RPCで生成 /// </summary> [PunRPC] private void PaintStartRPC(Vector3 inkPosition) { Instantiate(_inkPrefab, inkPosition, Quaternion.identity, _inkParent); } /// <summary> /// RPCで動かす /// </summary> [PunRPC] private void PaintingRPC(Vector3 inkPosition) { if (_inkParent.childCount > 0) { _inkParent.transform.GetChild(_inkParent.childCount - 1).transform.position = inkPosition; } } } 線の描画はTrailRendererを動かしているだけです。 【参考リンク】:【Unity(C#)】ハンドトラッキングで簡易版VRお絵かきアプリ おわりに 自己位置推定の精度を考えると、スマホで画像マーカーだけで位置合わせを支えるのは無理があるなーというのがやってみた感想です。 参考リンク ARSessionOrigin transform position and rotation to make created ARTrackedImage become Unity space zero Class ARSessionOrigin PUN2(Photon Unity Networking 2)で始めるオンラインゲーム開発入門
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#/WPF/MVVM/ReactiveUI] MVVMでダイアログ処理を行う(Interaction)

困っていたこと MVVM(Model-View-ViewModel)パターンに則ってアプリを作成していると、ダイアログの処理をどのように実装すればよいか困ることがあります。 例えばWPFにおいてファイル保存ダイアログ(SaveFileDialog)を表示してからファイルを保存したい場合、 Viewのコードビハインド(~.xaml.cs)にクリック時のイベントを実装しその中で呼び出す ViewModelのコマンド内でSaveFileDialogを呼び出す などが考えられます。 最も簡単なのは前者ですが、SaveFileDialogを呼び出したいためだけにViewにクリックイベントを実装するのは好ましくありません。 だからといって後者のようにViewModelにダイアログの処理を持ってくるのはMVVMパターンに反してしまいます。 Viewにダイアログの処理を記述し、ViewModelはその入出力だけを利用するような方法はないでしょうか? 解決策: Interactionを使ったダイアログ処理 .NETのMVVMフレームワークライブラリであるReactiveUIにはInteractionという機能を使うことによって解決できることがわかりました。 実装例 例題としてMVVMパターンに則ったWPFアプリでSaveFileDialogを使う場合を考えます。 プログラムの詳細はこちらで公開しています。 Model SaveFileDialogの例題なのでModelにはファイルを保存する機能を持たせます。 全体のコード Model using System; using System.IO; namespace MvvmDialog.Models { public class SaveText { public SaveTextResponse Save(SaveTextRequest request) { SaveTextResponse response = null; try { using (StreamWriter sw = new StreamWriter(request.FilePath, false)) { sw.Write(request.InputText); } response = new SaveTextResponse() { Succeed = true, Message = "File saved successfully." }; } catch (Exception ex) { response = new SaveTextResponse() { Succeed = false, Message = ex.Message }; } return response; } } public class SaveTextRequest { public string InputText { get; set; } public string FilePath { get; set; } } public class SaveTextResponse { public bool Succeed { get; set; } public string Message { get; set; } } } ViewModel 本題のSaveFileDialogのプロパティとしてInteractionを定義します。 public Interaction<string, string> SaveFileDialog { get; set; } Inputには起動時に表示されるファイル名が入力されるとし、またOutputとしてはファイルのフルパスを出力すると決めておきます。 ViewModel内でInteractionSaveFileDialogは次のように呼び出すことができます。 IObservable<string> output = SaveFileDialog.Handle(input); このInteractionを保存ボタン押下コマンド内に記述することで、ボタン押下→ダイアログ表示→保存処理を実現することができます。 全体のコード ViewModel using MvvmDialog.Models; using ReactiveUI; using ReactiveUI.Fody.Helpers; using System; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; namespace MvvmDialog.ViewModels { public class MainViewModel : ReactiveObject, IActivatableViewModel { [Reactive] public string InputText { get; set; } [Reactive] public string FileName { get; set; } [Reactive] public string ResultMessage { get; set; } public ReactiveCommand<Unit, SaveTextRequest> SaveFileCommand { get; private set; } public Interaction<string, string> SaveFileDialog { get; set; } public ViewModelActivator Activator { get; } public MainViewModel() { Activator = new ViewModelActivator(); this.WhenActivated(d => { HandleViewModelBound(d); }); } void HandleViewModelBound(CompositeDisposable d) { SaveFileDialog = new Interaction<string, string>(); SaveFileCommand = ReactiveCommand.CreateFromObservable(() => SaveFileDialog.Handle(FileName).Select(x => new SaveTextRequest() { FilePath = x, InputText = this.InputText })); SaveFileCommand.Select(x => new SaveText().Save(x)).Subscribe(x => ResultMessage = x.Message).DisposeWith(d); } } } View ViewではViewModelで定義したSaveFileDialogプロパティに具体的なInteractionの処理をバインドしていきます。 this.BindInteraction( ViewModel, vm => vm.SaveFileDialog, async interaction => { var result = await Task.Run(() => { var dialog = new SaveFileDialog() { FileName = interaction.Input, AddExtension = true, DefaultExt = "txt" }; if(dialog.ShowDialog()?? false) { return dialog.FileName; } else { return null; } }); interaction.SetOutput(result); }) .DisposeWith(d); 全体のコード View using Microsoft.Win32; using MvvmDialog.ViewModels; using ReactiveUI; using System.Reactive.Disposables; using System.Threading.Tasks; namespace MvvmDialog.Views { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : ReactiveWindow<MainViewModel> { public MainWindow() { InitializeComponent(); ViewModel = new MainViewModel(); this.WhenActivated(d => { HandleViewModelBound(d); }); } protected void HandleViewModelBound(CompositeDisposable d) { this.BindInteraction( ViewModel, vm => vm.SaveFileDialog, async interaction => { var result = await Task.Run(() => { var dialog = new SaveFileDialog() { FileName = interaction.Input, AddExtension = true, DefaultExt = "txt" }; if(dialog.ShowDialog()?? false) { return dialog.FileName; } else { return null; } }); interaction.SetOutput(result); }) .DisposeWith(d); this.OneWayBind(ViewModel, vm => vm.ResultMessage, v => v.ResultTextBlock.Text).DisposeWith(d); this.Bind(ViewModel, vm => vm.FileName, v => v.FileNameTextBox.Text).DisposeWith(d); this.Bind(ViewModel, vm => vm.InputText, v => v.InputTextBox.Text).DisposeWith(d); this.BindCommand(ViewModel, vm => vm.SaveFileCommand, v => v.SaveFileButton).DisposeWith(d); } } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む