- 投稿日:2020-07-30T21:42:05+09:00
Unity 2020で出てくるError detecting Visual Studio installations~への対処法
エラー文
Error detecting Visual Studio installations: System.ArgumentException: JSON parse error: Invalid escape character in string.
原因
Visual Studio Editor
パッケージが使用しているvswhere.exeから出力されるJsonに
一部Shift_JIS文字列が含まれるためパースでエラーが起きてるらしい対処法
Packages/com.unity.ide.visualstudio/Editor/Discovery.cs
のProcessインスタンス生成部分を修正して強制的にUTF-8にするDiscovery.csvar process = new Process { StartInfo = new ProcessStartInfo { FileName = progpath, //-utf8オプションで強制的にUTF-8として出力 //Arguments = "-prerelease -format json", Arguments = "-prerelease -format json -utf8", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, } };
- 投稿日:2020-07-30T18:06:34+09:00
(基礎 1-1)UnityでのC# のお作法 ~Input~
入力値のコードとしてinput
sample1.csInput.GetAxis("Horizontal");上記は -1 ~ 1 nの間の不動小数点で入力値が取得できる
(Horizontalなので左右の水平方向の数値)sample2.csInput.GetAxisRaw("Vertical");上記の記入方法だと-1または1のせい数値のみが取得できる値となる
(こちらはVerticalなので上下の垂直方向の数値)※初めての投稿なのでこんな感じでまずはやってみました
- 投稿日:2020-07-30T16:04:57+09:00
Unity動画ファイル最適化について
動画ファイルの最適化しようとした際に、以下の不思議な現象が発生しました。下図のInspectorに表示されるサイズ情報の変化はありませんが、ファイルサイズとProfile内のメモリサイズは確かに減少しました。ではInspectorに表示されているサイズはどういう意味でしょうか?
下図の動画ファイルにはScale曲線は含まれていません、今回の最適化処理は浮動小数点の精度を圧縮だけになりました。
動画ファイルの最適化前後のサイズを比較してみました。
FileSize
FileInfo.Lengthで取得したファイルサイズ
OSのファイルシステムで確認できるファイルサイズMemorySize
Profiler.GetRuntimeMemorySizeで取得したメモリサイズ
Profilerでサンプリングして取得しました
それぞれ実機およびEditorでサンプリングしましたBlobSize
反射で取得したAnimationClip.sizeのバイナリーサイズ
AnimationClipのInspectorのパネル上に表示されるサイズ
赤枠内はBlobSize,こちらの認識では、FileSizeはそのファイルがハードディスク上に占めているファイルサイズ、BlobSizeはファイルをデシリアライズしたオブジェクトのバイナリーサイズです。Editor内のMemorySizeはシリアライズした後のメモリサイズだけではなく、オリジナルファイルのメモリサイズも一つ維持いしている。これはEditorに一つTextureをロードした際にメモリサイズが二つと同じことです。しかし、実機ではほぼBlobSizeに等しいです。実機でのMemorySizeとInspector内のBlobSizeは非常に近い、BlobSizeは実機上のメモリサイズと同じと考えてもよい、参考用の価値はあると思います。
同時に、Scale曲線の取り除く方法にも実験しました。下図の動画ファイルは本来InspectorでのScaleの値は4、つまりScale曲線が存在します。オリジナルファイルのBlobSizeが10.2KB、 Scale曲線を取り除いた後、Blob Sizeが7.4KBに変わったため、BlobSizeが27%を減少しました。
Curveの減少がメモリサイズの減少に繋がります
上述の実験で分かるように、動画ファイルの圧縮精度をカットするだけで、Curveの減少になりません。浮動小数点数はすべて32bitを固定で占められているから、BlobSizeは何の変化もありません。しかしファイルサイズ、ABサイズ、Editor内のメモリサイズは、精度を圧縮後、Curveの変化有無にかかわらず、すべて小さくなります。
動画ファイルの精度をカットすれば、サンプルの位置も変わるということで、Constant CurveとDense Curveの数量も変わる可能性があります。精度をカットしたことにより動画のサンプルは薄くなりますが、連続の同じサンプルが増えました。だからDense Curveが減少し、Constant Curveが増え、合計のメモリサイズが減少になりました。
Constant Curveは一番左側のサンプルだけで一つの曲線ブロックを表現できる。
精度をカットのみでBlobSize減少させる実例
精度カット前、サイズは2.2kb、ScaleCurveは0、 ConstantCurveは4(57.1%)、Stream(Optimalモード使用したデータはDenseとして保存される)は3(42.9%)。
精度カット後、サイズは2.1kb、ConstantCurveは7(100%)、Streamは0(0%)。カット後、ConstantCurveを3増加させたが、Stream(Optimalモード下ではDense)は3が減少しました、BlobSizeは0.1kb減少になりました。
ここでわかるように、精度を通じての最適化方法は、その本質は曲線上あまり近い数値(例、相違数値が浮動小数点4桁以降に現れた場合)を直接同じ数値に変えることによって、一部の曲線をconstant曲線に変更し、メモリサイズを減少させることです。結果
プロジェクトチームからのフィードバックによると、全ての動画ファイルに対して最適化を行いました。それでファイルサイズは820MB->225MB, ABサイズは72MB->64MB,メモリサイズは50MB->40MBになりました。全体的に言えば動画ファイルのscaleが多ければ、最適化を行う効果を得られやすいとのことです。
BlobSizeコード
BlobSizeAnimationClip aniClip = AssetDatabase.LoadAssetAtPath<AnimationClip> (path); var fileInfo = new System.IO.FileInfo(path); Debug.Log(fileInfo.Length);//FileSize Debug.Log(Profiler.GetRuntimeMemorySize (aniClip));//MemorySize Assembly asm = Assembly.GetAssembly(typeof(Editor)); MethodInfo getAnimationClipStats = typeof(AnimationUtility).GetMethod("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic); Type aniclipstats = asm.GetType("UnityEditor.AnimationClipStats"); FieldInfo sizeInfo = aniclipstats.GetField ("size", BindingFlags.Public | BindingFlags.Instance); var stats = getAnimationClipStats.Invoke(null, new object[]{aniClip}); Debug.Log(EditorUtility.FormatBytes((int)sizeInfo.GetValue(stats)));//BlobSizeツールのコード
最後にツールのコードと簡単な説明を加えます。最適化を行いたいフォルダーもしくはファイルを選定し、右クリックAnimation->浮動小数点カットおよびScaleを取り除きます。
//**************************************************************************** // // File: OptimizeAnimationClipTool.cs // // Copyright (c) SuiJiaBin // // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF // ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A // PARTICULAR PURPOSE. // //**************************************************************************** using System; using System.Collections.Generic; using UnityEngine; using System.Reflection; using UnityEditor; using System.IO; namespace EditorTool { class AnimationOpt { static Dictionary<uint,string> _FLOAT_FORMAT; static MethodInfo getAnimationClipStats; static FieldInfo sizeInfo; static object[] _param = new object[1]; static AnimationOpt () { _FLOAT_FORMAT = new Dictionary<uint, string> (); for (uint i = 1; i < 6; i++) { _FLOAT_FORMAT.Add (i, "f" + i.ToString ()); } Assembly asm = Assembly.GetAssembly (typeof(Editor)); getAnimationClipStats = typeof(AnimationUtility).GetMethod ("GetAnimationClipStats", BindingFlags.Static | BindingFlags.NonPublic); Type aniclipstats = asm.GetType ("UnityEditor.AnimationClipStats"); sizeInfo = aniclipstats.GetField ("size", BindingFlags.Public | BindingFlags.Instance); } AnimationClip _clip; string _path; public string path { get{ return _path;} } public long originFileSize { get; private set; } public int originMemorySize { get; private set; } public int originInspectorSize { get; private set; } public long optFileSize { get; private set; } public int optMemorySize { get; private set; } public int optInspectorSize { get; private set; } public AnimationOpt (string path, AnimationClip clip) { _path = path; _clip = clip; _GetOriginSize (); } void _GetOriginSize () { originFileSize = _GetFileZie (); originMemorySize = _GetMemSize (); originInspectorSize = _GetInspectorSize (); } void _GetOptSize () { optFileSize = _GetFileZie (); optMemorySize = _GetMemSize (); optInspectorSize = _GetInspectorSize (); } long _GetFileZie () { FileInfo fi = new FileInfo (_path); return fi.Length; } int _GetMemSize () { return Profiler.GetRuntimeMemorySize (_clip); } int _GetInspectorSize () { _param [0] = _clip; var stats = getAnimationClipStats.Invoke (null, _param); return (int)sizeInfo.GetValue (stats); } void _OptmizeAnimationScaleCurve () { if (_clip != null) { //scale曲線を取り除く foreach (EditorCurveBinding theCurveBinding in AnimationUtility.GetCurveBindings(_clip)) { string name = theCurveBinding.propertyName.ToLower (); if (name.Contains ("scale")) { AnimationUtility.SetEditorCurve (_clip, theCurveBinding, null); Debug.LogFormat ("{0}のscale curveを閉じる", _clip.name); } } } } void _OptmizeAnimationFloat_X (uint x) { if (_clip != null && x > 0) { //浮動小数点精度をf3まで圧縮する AnimationClipCurveData[] curves = null; curves = AnimationUtility.GetAllCurves (_clip); Keyframe key; Keyframe[] keyFrames; string floatFormat; if (_FLOAT_FORMAT.TryGetValue (x, out floatFormat)) { if (curves != null && curves.Length > 0) { for (int ii = 0; ii < curves.Length; ++ii) { AnimationClipCurveData curveDate = curves [ii]; if (curveDate.curve == null || curveDate.curve.keys == null) { //Debug.LogWarning(string.Format("AnimationClipCurveData {0} don't have curve; Animation name {1} ", curveDate, animationPath)); continue; } keyFrames = curveDate.curve.keys; for (int i = 0; i < keyFrames.Length; i++) { key = keyFrames [i]; key.value = float.Parse (key.value.ToString (floatFormat)); key.inTangent = float.Parse (key.inTangent.ToString (floatFormat)); key.outTangent = float.Parse (key.outTangent.ToString (floatFormat)); keyFrames [i] = key; } curveDate.curve.keys = keyFrames; _clip.SetCurve (curveDate.path, curveDate.type, curveDate.propertyName, curveDate.curve); } } } else { Debug.LogErrorFormat ("現在{0}位浮動小数点をサポートしません", x); } } } public void Optimize (bool scaleOpt, uint floatSize) { if (scaleOpt) { _OptmizeAnimationScaleCurve (); } _OptmizeAnimationFloat_X (floatSize); _GetOptSize (); } public void Optimize_Scale_Float3 () { Optimize (true, 3); } public void LogOrigin () { _logSize (originFileSize, originMemorySize, originInspectorSize); } public void LogOpt () { _logSize (optFileSize, optMemorySize, optInspectorSize); } public void LogDelta () { } void _logSize (long fileSize, int memSize, int inspectorSize) { Debug.LogFormat ("{0} \nSize=[ {1} ]", _path, string.Format ("FSize={0} ; Mem->{1} ; inspector->{2}", EditorUtility.FormatBytes (fileSize), EditorUtility.FormatBytes (memSize), EditorUtility.FormatBytes (inspectorSize))); } } public class OptimizeAnimationClipTool { static List<AnimationOpt> _AnimOptList = new List<AnimationOpt> (); static List<string> _Errors = new List<string>(); static int _Index = 0; [MenuItem("Assets/Animation/浮動小数数をカットし、Scaleを取り除く")] public static void Optimize() { _AnimOptList = FindAnims (); if (_AnimOptList.Count > 0) { _Index = 0; _Errors.Clear (); EditorApplication.update = ScanAnimationClip; } } private static void ScanAnimationClip() { AnimationOpt _AnimOpt = _AnimOptList[_Index]; bool isCancel = EditorUtility.DisplayCancelableProgressBar("优化AnimationClip", _AnimOpt.path, (float)_Index / (float)_AnimOptList.Count); _AnimOpt.Optimize_Scale_Float3(); _Index++; if (isCancel || _Index >= _AnimOptList.Count) { EditorUtility.ClearProgressBar(); Debug.Log(string.Format("—最適化完了-- エラー数: {0} 合計数: {1}/{2} エラーメッセージ↓:\n{3}\n----------アウトプット完了----------", _Errors.Count, _Index, _AnimOptList.Count, string.Join(string.Empty, _Errors.ToArray()))); Resources.UnloadUnusedAssets(); GC.Collect(); AssetDatabase.SaveAssets(); EditorApplication.update = null; _AnimOptList.Clear(); _cachedOpts.Clear (); _Index = 0; } } static Dictionary<string,AnimationOpt> _cachedOpts = new Dictionary<string, AnimationOpt> (); static AnimationOpt _GetNewAOpt (string path) { AnimationOpt opt = null; if (!_cachedOpts.ContainsKey(path)) { AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip> (path); if (clip != null) { opt = new AnimationOpt (path, clip); _cachedOpts [path] = opt; } } return opt; } static List<AnimationOpt> FindAnims() { string[] guids = null; List<string> path = new List<string>(); List<AnimationOpt> assets = new List<AnimationOpt> (); UnityEngine.Object[] objs = Selection.GetFiltered(typeof(object), SelectionMode.Assets); if (objs.Length > 0) { for(int i = 0; i < objs.Length; i++) { if (objs [i].GetType () == typeof(AnimationClip)) { string p = AssetDatabase.GetAssetPath (objs [i]); AnimationOpt animopt = _GetNewAOpt (p); if (animopt != null) assets.Add (animopt); } else path.Add(AssetDatabase.GetAssetPath (objs [i])); } if(path.Count > 0) guids = AssetDatabase.FindAssets (string.Format ("t:{0}", typeof(AnimationClip).ToString().Replace("UnityEngine.", "")), path.ToArray()); else guids = new string[]{}; } for(int i = 0; i < guids.Length; i++) { string assetPath = AssetDatabase.GUIDToAssetPath (guids [i]); AnimationOpt animopt = _GetNewAOpt (assetPath); if (animopt != null) assets.Add (animopt); } return assets; } } }
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析と最適化ソリューション及びコンサルティングサービスを提供している会社でございます。
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
- 投稿日:2020-07-30T15:43:55+09:00
あなたのモバイルゲーム開発の最適化時間を数ヶ月節約する方法(メモリ篇)
みなさん、こんにちは。こちらはUWA Technologies(略称UWA)です。
今日は「あなたのモバイルゲーム開発の最適化時間を数ヶ月節約する方法」の最後のパート「メモリ」についてご説明いたします。
前回の文章をまだ読まれていない皆様は、是非移動してご覧ください。
「あなたのモバイルゲーム開発の最適化時間を数ヶ月節約する方法(レンダリング篇)」
「あなたのモバイルゲーム開発の最適化時間を数ヶ月節約する方法(UI+ロード篇)」メモリ
メモリパートに最も重要なのはメモリの割り当てとメモリリークです。
ほとんどのプロジェクトでは、アセットメモリとMonoメモリという2つの大きなメモリ割り当てがあります。 ここでは、最初にアセットメモリについて説明します。アセットメモリ
UWAレポートでは、テスト中にテクスチャメモリの割り当てを記録できます。 大事なのは、Monoメモリとテクスチャフォーマットです。
また、メモリの詳細を知りたい場合は、クリックして詳細を表示できます。
実例
これはゲーム「Kiwame」からのデータです。テクスチャの詳細情報を見ることができます。 この中に、「アセット名」、「メモリ割り当て」、「解像度」、「フォーマット」などが含まれます。NewUI1_rgbが16 MBで非常に大きいことがわかります。 開発チームはそれを チェックして、最適化する必要があるかどうかを確認できます。
また、アセット管理にも注意する必要があります。表から1つ以上のアセットを選択、それらの使用情報がチャートに表示されます。アセットがいつロードされ、いつアンロードされるかを確認でき、メモリリークを分析することには非常に有用であります。
メッシュ
次にメッシュの詳細を見に行きます。
アニメーションクリップ、オーディオクリップ、シェーダー、フォント、RT、パーティクルシステムなど、すべての詳細情報を直接かつ迅速に見ることができます。結果
Monoメモリ
このように、全体的なMonoメモリ割り当てに注目すべき、メモリの合理性を確認する必要があります。
UWAレポートは、関数のコールスタックと詳細なメモリ割り当てを示します。それで、重要な関数名をすばやく確認できし、コードで検索して最適化することができます。結果
こちらは結果です。25日をかかって、Monoメモリは150MBから50MBに最適化されました。
メモリリーク
メモリ部分に、非常に難しい難問があります。それはメモリリークです。以前には、メモリリークを分析して解決するには、2〜3か月は常に必要でした。
UWAレポートでは、常駐Monoメモリを表示できます。グラフにMonoメモリがこのように増える一方で、下がらない場合には、つまり、メモリリークが発生したということです。
じゃあどうすれば修正できますか? チャートの2つのサンプルを比較して、Monoメモリの増分を見つけます。
このプロジェクトでは、メモリのほとんどの増加が、InstantateGamObjectという関数のInstantiateによるものであることがわかります。
これらは、garbage collectedできない変異体です。 そして、開発チームは関数を直接チェックして、どのコンテナーがそれらを参照しているかを確認できます。その後、コードをすばやく最適化できます。結果
これが結果です。 3日後、このプロジェクトでは、メモリリークの問題がほぼなくなりました。
これは別の例です。 ただ1日で、メモリリークの問題が改善しました。現在、中国でUWA GOTを使っているゲームプロジェクトは、メモリリークの問題は約3日で大幅に改善できます。
過去のメモリリークの割合は約50%でしたが、昨年、UWA GOTでMonoメモリ分析を開始して以来、割合は25%に低下しています。今回、UWA GOTが日本のゲーム開発者たちに役立つことを願っています。
UWAおよびUWA GOTについてもっと了解したい場合には、UWA公式サイトに移動してください。
UWA GOTを使っていたnowsprintingさんも自身のブログでUWA GOTについて詳しく説明してしましたが、興味があれば「やらなイカ?」に移動してご覧ください。
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析と最適化ソリューション及びコンサルティングサービスを提供している会社でございます。
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
- 投稿日:2020-07-30T15:42:04+09:00
Unity 3D入門 #11 [マウスによる一人称視点移動]
パソコンの一人称視点ゲームらしくマウスで視点変更できるようにします。
参考URL:https://mslgt.hatenablog.com/entry/2017/02/18/020630
上の手法ではクリックしている間しかカメラの移動ができないので、常にマウスの位置を追う実装にします。
private Vector3 lastMousePosition; private Vector3 newAngle = new Vector3(0, 0, 0); void Start() { newAngle = this.transform.localEulerAngles; lastMousePosition = Input.mousePosition; } void Update() { newAngle.y += (Input.mousePosition.x - lastMousePosition.x) * y_rotate * x_reverce; newAngle.x -= (Input.mousePosition.y - lastMousePosition.y) * x_rotate * y_reverce; this.gameObject.transform.localEulerAngles = newAngle; lastMousePosition = Input.mousePosition; }x_rotate, y_rotateはマウスによるカメラ回転の感度を、x_reverce, y_reverceは設定による反転(マウスを上に移動したら下に向くようにする)のための変数。
しかし、この実装ではマウスポインタがはじめから中央に無いと操作に違和感が出るかもしれません。(要検討)
また、一人称視点でカメラを回転させた時、カメラが壁を貫通して向こう側が見えるので、カメラが壁を貫通しないようにします。
カメラはプレイヤーと同じx,z座標に浮かんでいるので、プレイヤーのColliderの当たり判定を大きくしてプレイヤーが一定以上壁に近づかないようにするとカメラも壁にめり込むことはなくなりました。(もし、攻撃された時などの当たり判定は新しくそれ用のColliderを作ればOKのはず)
- 投稿日:2020-07-30T13:29:37+09:00
Unity 3D入門 #10 [Skyboxの非表示 &円形のゲージ実装]
unityの地平線が少し明るくなっていることが気になるので、プレイヤーが持つ懐中電灯以外の光源を全て消したいと思います。
unityでは広大に広がる世界を見せるために画面全体をskyboxというラッパーで覆われています。そのため、地平線にはskyboxがあり、それが夕暮れのような地平線を演出しています。
http://ws.cis.sojo-u.ac.jp/~izumi/Unity_Documentation_jp/Documentation/Manual/Cameras.html のクリアフラグの項目を見ればわかりますが、デフォルトの設定ではスクリーン上の描き残された部分はskyboxが表示されるようになっています。
そのため、メインカメラのClear Flagsの設定をSkyboxからSolid Colorに変更し、BackGroundを黒色にします。
これで、地平線が真っ暗になりました。
また、ドアのロックを解除する時などに利用する円形のゲージの作成を行います。
参考URL:https://clrmemory.com/programming/unity/circle-gauge-meter-p1/上の画像を用いてゲージを作成します。(qiita上ではうまく表示されませんが、1枚目はダウンロードすると透過背景に白いドーナツ状の円の画像となっています)
UI->Imageのsource imageに作ったpngファイルが入らない場合は、そのpngファイルのTexture TypeをSprite(2D and UI)に変更してください。
参考URL:https://teratail.com/questions/98383また、スクリプトでUI->Imageを取得したい場合(参考URL:http://chnr.hatenablog.com/entry/2015/03/17/130223)
private Image gauge; void Start(){ gauge = GameObject.Find("gauge").GetComponent<Image>(); }
- 投稿日:2020-07-30T12:51:25+09:00
Vuforia EngineのArea Targetsを使ってみた
まえがき
Vuforia EngineのArea Targetsという機能を用いて、オフィスの共用部分をスキャンしてオブジェクトを表示してみました。
Area Targetsは、スキャンした空間全体をターゲットにすることができる機能です。
2020/7/11現在では、Matterport Pro2という3Dカメラでスキャンした空間のみターゲットにすることができます。
また、Matteport Pro2でスキャンしたデータをArea Targetsで利用するためには、Matterport Pro Professional(69ドル/月)と、スキャンデータをダウンロードするのにMatter Pak(Professional Planでは49ドル)が必要です。
また、Matterport側にAPIの利用申請をメールで送っておく必要があります。
実行環境
- Unity 2019.3.0f3
- Matterport Pro2
- Vuforia Engine 9.0
- iPhone 11 Pro
Vuforia Engineとは
Vuforia Engineは、iOSやAndroid、PC、HoloLensといったAR/MRデバイスに対応したAR開発プラットフォームです。
Vuforia EngineはPTCが提供していますが、元々はQualcommが開発しており、2015年にPTCがVuforia事業を6500万ドルで買収しています。
マーカートラッキングを非常に得意としており、特徴点の多い(複雑な)画像であればほとんどぶれることなく3Dオブジェクトを配置することができます。
詳しくはこちらをご覧ください!
画像をターゲットにするImage Targetsを使っていきたいと思います。
ライセンスの取得とライセンスキーをUnityに設定する方法関しては、ラズパイ4をVuforia Engineで分身させてみた ~Object Recognition編~こちらをご覧ください。
Area Targetsとは
Image TargetsやModel Targetsは視界に捉えられる被写体に対するものに限られており、広い空間をトラッキングするために設計されたものではありません。
Area Targetsはスキャンした空間をトラッキング対象として、AR表現を行うことが可能になります。
今回は、このようにオフィスの共用部をスキャンしてArea Targetsを使用してみました。
スキャンからUnityで実行するまで
Matterport Pro2による撮影
シェアオフィスの共用エリアをMatterport Pro2を用いて3Dスキャンしました。
画像のようにMatterport Pro2を設置します。
そして、iPhoneのMatterport Captureというアプリでスキャンを始めることができます。
実際に撮影している風景がこちらです。
Matterport Pro2によるスキャン風景 pic.twitter.com/a2fIDL7gwX
— こーや (@koyataroo) July 19, 202060度ずつ回転してスキャンをしているようです。
実際に撮影する場合は、自分が写ったらダメなのでこのように物陰に隠れて撮影をします。
物陰に隠れてMatterport Pro2で撮影 pic.twitter.com/PhSKcJOfyL
— こーや (@koyataroo) July 30, 2020複数箇所スキャンすると、以下のようにアプリ上にスキャン結果が表示されます。
公式では、2m間隔でスキャンすることが推奨されています。
窓の位置は、手動で設定をしました。
そうすることで窓ガラスの外を認識してしまった場合、不要な部分は削除することができます。
スキャンデータをクラウドにアップロード
スキャンデータをMatterportのサーバーにアップロードすることで、スキャンデータを作成することができます。
ここで、Matteport Pro2で撮影したデータをアップロードする場合には、69ドル/月のProfessional Planに加入している必要があります。
アプリからアップロードすることができ、3-4時間でスキャンデータの作成が完了しました。
スキャンデータをArea Targetsで利用できるようにする
スキャンデータをクラウドからダウンロードするには、49ドルが必要になります。(Matter Pak)
Matter Pakの購入後、Area Target Generatorを用いてスキャンデータをArea Targetsで利用できる形式に変換します。
MatterportにAPI利用申請のメールを送って承認されると、Tokenが取得できるのでそれを入力します。
Scanned Spaceには、クラウド上のスキャンデータのIDを入力します。(どこにあるのかわからず、URLの???の部分を入力したらいけましたhttps://my.matterport.com/models/???)
すると.unitypackageファイルを作成することができます。
Area TargetsをUnityで使う
Unityでプロジェクトを作成した後、先ほどの.unitypackageを読み込みます。
すると、Area Targetsを配置することができます。
あとは通常の開発同様、Area Targets配下にオブジェクトを配置していくといった流れです。
Area Targetsを用いて実装
作成したもの
Vuforia Area Targetsのデモ pic.twitter.com/uAspSz4Al3
— こーや (@koyataroo) July 19, 2020実装内容
- 室内の温度の表示
実際の気温とは関係なく、1秒おきに変化させています。
今回は行っていませんが、IoTデバイスと連携して、温度センサー情報を取得して表示をする想定です。
アニメーション付きで変化させてみると見栄えもよくなりそうだと感じました。
- 座席の予約切り替え
座席のエリアをタップすることで、AvailableとReservedの切り替えをすることができます。
- プロジェクターの予約切り替え
プロジェクター横のレバーをタップして切り替えることで、プロジェクターの使用可否を切り替えることができます。
- 危険エリアの表示
Dangerと表示されているエリアは侵入禁止エリアを想定しています。
- 目的地(プリンタ)へのナビゲーション
Find Printerボタンを押すと、プリンタの位置を矢印が差してくれます。
Area Targetsによって、カメラがどちらの方向を向いているのかがわかるために実現できています。
ルートを指し示す場合は、通ることのできる道の定義、ルート探索のアルゴリズムを実装することで実現できます。
オクルージョン
オクルージョンとは、現実世界の物体の奥に3Dオブジェクトが存在する場合に、重なっている3Dオブジェクトを表示しない機能です。
こちらの画像のように、壁の奥のDangerの部分は隠れる機能です。
Area TargetsのSimulate Occlusionをオンにすると、オクルージョンされるようになります。
この場合のオクルージョンは、スキャン時に存在していた物体に対してのみ適用されます。
そのため、スキャン時に存在していなかった物体が存在していたり、スキャン時にあったものがなくなってしまった場合、少し違和感のあるオクルージョンが発生します。
スキャン時に存在していなかった物体が存在する場合は、その物体に対してのオクルージョンが行われません。
また、スキャン時にあったものがなくなってしまった場合は、スキャン時の位置に存在していた物体通りにオクルージョンが行われます。
なので、基本的には机など動く可能性のあるものはスキャンに含めない方がよいかと思います。
Image Targetとの併用
Vuforia Area TargetsとImage Targetsの併用 pic.twitter.com/ut2PWUj5ga
— こーや (@koyataroo) July 30, 2020このように、Area Targetsを利用しているときに、別のTargetも使用することができます。
空間に配置するものはArea Targetsで、何か物体やマーカーに動的に追随するものはImage TargetsやModel Targetsを使うといったユースケースが考えられます。
まとめ
Vuforia EngineのArea Targetsは、精度よくオブジェクトを配置することができました。
Area Targetsは、オフィスや、工場、美術館といった場所で、ゲームやナビゲーションを表示するのにとても有用であると感じました。
現状では、Matterport Pro2と月額69ドルが必要であるため試すのにもハードルがありますが、試す価値は十分にあると思います!
開発者向けのVuforia Slackコミュニティがあり、僕も参加しているので、記事の内容で質問があればそちらから質問下さい!
https://www.it-ex.com/promo/vuforia/to-developers/
また今回のコードはGithubに載せているので是非クローンしてお手元で試してみてください!
- 投稿日:2020-07-30T01:03:07+09:00
Unity 3D入門 #9 [ドアの開閉 ]
ドアの前で特定のボタンを押すことでドアの開閉ができるようにします。
Sun Templeというアセットの中のDoorというスクリプトをもとに作っていきます。
- namespace SunTempleを消すためにはSun Templeのスクリプト「CircularLerp.cs」の関数[public static float Clerp(float start, float end, float value)]をDoor.csにコピペする必要があります。
public bool IsLocked = false; //ドアに鍵がかかっているか public bool DoorClosed = true; //ドアが現在閉まっているか public float OpenRotationAmount = 90; //ドアが何度開くか(90で直角に開く) public float RotationSpeed = 1f; //ドアが開く速度 public float MaxDistance = 3.0f; //ドアからどれほど離れていても開けられるか private Collider DoorCollider; //ドアのコライダー(DoorCollider.enabled = falseで当たり判定消失) private GameObject Player; //ドアを開けるプレイヤーのGameObject private Camera Cam; //メインカメラ float StartAngle = 0; float EndAngle = 0; float LerpTime = 1f; //ドアを開ける際にかかる時間 float CurrentLerpTime = 0; //ドアを開ける際の現在の移動時間 bool Rotating; void Start(){ DoorCollider = GetComponent<BoxCollider> (); Player = GameObject.FindGameObjectWithTag ("Player"); Cam = Camera.main; } void Update(){ if (Rotating) { Rotate (); } if(Input.GetKeyDown(KeyCode.M)){ TryToOpen (); } } void TryToOpen(){ if (Mathf.Abs(Vector3.Distance(transform.position, Player.transform.position)) <= MaxDistance){ Ray ray = Cam.ScreenPointToRay (new Vector3 (Screen.width / 2, Screen.height / 2, 0)); RaycastHit hit; if (DoorCollider.Raycast(ray, out hit, MaxDistance)){ if (IsLocked == false){ if (DoorClosed) Open(); else Close(); } } } } void Rotate(){ CurrentLerpTime += Time.deltaTime * RotationSpeed; if (CurrentLerpTime > LerpTime){ CurrentLerpTime = LerpTime; } float _Perc = CurrentLerpTime / LerpTime; //今全体の何割進んでいるか float _Angle = Clerp(StartAngle, EndAngle, _Perc); //今現在の角度 transform.localEulerAngles = new Vector3(transform.eulerAngles.x, _Angle, transform.eulerAngles.z); if (CurrentLerpTime == LerpTime) { //回転し終えたら Rotating = false; DoorCollider.enabled = true; } } void Open(){ DoorCollider.enabled = false; //コライダーを無効化 DoorClosed = false; StartAngle = transform.localEulerAngles.y; EndAngle = transform.localEulerAngles.y + OpenRotationAmount; CurrentLerpTime = 0; Rotating = true; } void Close(){ DoorCollider.enabled = false; DoorClosed = true; StartAngle = transform.localEulerAngles.y; EndAngle = transform.localEulerAngles.y - OpenRotationAmount; CurrentLerpTime = 0; Rotating = true; } public static float Clerp(float start, float end, float value){ float min = 0.0f; float max = 360.0f; float half = Mathf.Abs((max - min) / 2.0f);//half the distance between min and max float retval = 0.0f; float diff = 0.0f; if ((end - start) < -half){ diff = ((max - start) + end) * value; retval = start + diff; } else if ((end - start) > half) { diff = -((max - end) + start) * value; retval = start + diff; } else retval = start + (end - start) * value; return retval; }上の長いコードの分からなかったところ
Cam.ScreenPointToRay(Vector3 pos) posからカメラの向いている方向に向けてRayを発生させる(https://developer.roblox.com/en-us/api-reference/function/Camera/ScreenPointToRay)
lerpについて 二点間の線形補間
lerp参考URL:https://www.sejuku.net/blog/83510
線形補間参考URL:https://qiita.com/niusounds/items/c4af702b06582590c82e関数Clerpについて value : 全体の何割進んでいるか, retval : startとendの間となる線形補間された回転角度(下にイメージ図)