20200103のUnityに関する記事は9件です。

【Unity】ARで何か表示してみようと思ったけど結局躓いて解決できなかった件【Vuforia】

テスト環境

・PC:Windows10
・Unity 2019.2.5f1
・Unity 2018.1.9f2
・Vuforia 8.6.7
・Xperia Z3

Vuforiaのインストールから諸々

ネットで色々なところから情報をかき集め、デベロッパー登録やらなんやらを済ませた。
参考にさせてもらった記事とか
https://xr-hub.com/archives/8508
http://pg-box.jp/blog/20140909/ar#AR-3
https://qiita.com/broken55/items/12144825ea1e8b8822b3

躓いたこと

使ったことある人ならわかると思いますが、大体書いてあることはどこも一緒で、登録したらキー発行してここに入れましょうねーとか、ヒエラルキーはこうしましょうねーというところだったのでそこは迷わずできた。
どうしても解決できなかったのは、ARCameraを一つしかヒエラルキーに置いていないのに、twocameraなんちゃらーっていうエラーが出る。
調べてみると、カメラは一つしか使えませんみたいなエラーらしく、英語で質問しているページがあったので翻訳をして調べてみたが結局直し方まではたどり着けず…

Unity公式からサンプルが出ているっぽかったのでそれを流用して作ってしまおうかなとも思ったんですが、バージョン的に対応していないのか、いろんなバージョンでインポートしてもエラー吐きまくりとか…

技術力不足で解決に至りませんでした。

次はARKitとか使ってみようと思います。
解決策知っている人がいたら教えてください。

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

【Unity学习笔记】脚本优化小技巧分享

前言

Unity游戏程序的优化一直以来都是令人棘手的问题。与排查Bug的过程不同,造成游戏帧率低下卡顿的问题可能分散在程序的各个角落,而每一个小问题单独看却又不够明显难以察觉。因此比起在开发后期再进行优化而困难重重,应该从一开始就注重效率来开发。
Unity优化在于不少方面,本文介绍几个在写脚本时容易被忽视的优化小技巧。

1. 使用transform应该先缓存

一个继承自MonoBehavior的脚本可以直接通过transform属性访问到自身物体的Transform组件,然而这个行为的效率其实不高。如果在一帧内需要大量对Transform进行操作的话,则应该先建立一个Transform的缓存,对缓存进行操作而不是每次都访问transform属性。
我们进行直接访问和通过缓存访问的对比试验。

Test.cs
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("ChangePosition");
    // 为使实验结果明显,反复执行1000000次操作
    for (int i = 0; i < 1000000; i++)
    {
        transform.position = new Vector3(i, i, i);            
    }
    Profiler.EndSample();
}

コメント 2020-01-03 192333.png
通过直接访问transform来改变position1000000次,耗时91.52ms。

Test.cs
private Transform myTransform;

private void Start()
{
    myTransform = transform;
}

private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("ChangePosition");
    // 为使实验结果明显,反复执行1000000次操作
    for (int i = 0; i < 1000000; i++)
    {
        myTransform.position = new Vector3(i, i, i);            
    }
    Profiler.EndSample();
}

コメント 2020-01-03 193055.png
首先建立一个名为myTransform的私有变量,在start中访问transform一次赋值给myTransform。通过访问myTransform来改变position1000000次,耗时55.12ms。
结论是,通过缓存transform的方式可以比直接访问transform的运行效率提高了约40%。这个优化技巧在场景中有大量Transform操作时的效果十分明显。

2. 不要频繁访问Camera.main

使用Camera.main虽然可以很方便的访问到主相机,但其内部本质是执行了FindGameObjectWithTag("MainCamera"),效率是非常低下的。

Test.cs
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("Camera Operation");
    // 为使实验结果明显,反复执行100000次操作
    for (int i = 0; i < 100000; i++)
    {
        Camera.main.fieldOfView = 60f;            
    }
    Profiler.EndSample();
}

コメント 2020-01-03 202413.png

通过执行Camera.main获取相机,设置FOV100000次,耗时33.99ms。

Test.cs
private Camera myCamera;

private void Start()
{
    myCamera = Camera.main;
}

private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("Camera Operation");
    // 为使实验结果明显,反复执行100000次操作
    for (int i = 0; i < 100000; i++)
    {
        myCamera.fieldOfView = 60f;
    }
    Profiler.EndSample();
}

コメント 2020-01-03 202632.png
创建一个myCamera的变量来缓存Camera.main,通过访问myCamera来设置FOV100000次,耗时仅5.61ms。
结论是,创建一个变量把Camera.main缓存下来,通过访问缓存来相机,效率会提高约83%。

3. 创建向量时,尽量使用new Vector(),而不要使用Vector.zero等属性

Unity的向量类中提供的方便创建向量的属性,例如Vector3.zero,Vector.up等属性的效率其实并不高。

Test.cs
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("New Vector");
    // 为使实验结果明显,反复执行1000000次操作
    for (int i = 0; i < 1000000; i++)
    {
        var vector = Vector3.zero;   
    }
    Profiler.EndSample();
}

コメント 2020-01-03 205855.png
使用Vector3.zero来创建向量1000000次,耗时11.70ms

Test.cs
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("New Vector");
    // 为使实验结果明显,反复执行1000000次操作
    for (int i = 0; i < 1000000; i++)
    {
        var vector = new Vector3();   
    }
    Profiler.EndSample();
}

コメント 2020-01-03 210148.png
直接使用new Vector3()来创建向量1000000次,耗时仅3.55ms。
结论是使用new Vector()而不是Vector.zero等属性来创建向量,效率提升约70%。

4. position和localPosition等同时,尽可能使用localPosition(rotation同理)

为了方便管理Hierarchy,我们经常会创建position和rotation都等于0的父节点,将需要管理的物体放进去作为子物体。这些子物体的localPosition和position在值上是等同的,但在访问position时实际上是执行了将localPosition转换到世界坐标系的操作,效率上比直接访问localPosition低。
コメント 2020-01-03 220436.png
先创建一个5层结构,在每一层的物体上执行测试脚本。

Test.cs
private Transform myTransform;
private void Start()
{
    myTransform = transform;
}
private void Update()
{
    // 让Profiler监视这一段操作
    Profiler.BeginSample("SetPosition");
    // 为使实验结果明显,反复执行1000000次操作
    for (int i = 0; i < 100000; i++)
    {
        myTransform.position = new Vector3(i, i, i);
    }
    Profiler.EndSample();

    // 让Profiler监视这一段操作
    Profiler.BeginSample("SetLocalPosition");
    // 为使实验结果明显,反复执行1000000次操作
    for (int i = 0; i < 100000; i++)
    {
        myTransform.localPosition = new Vector3(i, i, i);
    }
    Profiler.EndSample();
}

根节点的执行情况
コメント 2020-01-03 221248.png
第一层子节点的执行情况
コメント 2020-01-03 221322.png
第二层子节点的执行情况
コメント 2020-01-03 221402.png
第三层子节点的执行情况
コメント 2020-01-03 221435.png
第四层子节点的执行情况
コメント 2020-01-03 222740.png
我们可以看到,层级越深的物体访问position时坐标系转换的耗时就越长。

总结

  1. transform先缓存再使用。
  2. Camera.main先缓存再使用。
  3. 创建向量使用new Vector(),而不是Vector.zero等属性。
  4. 尽可能使用localPosition而不是position,rotation同理。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マスターデータ運用のベストプラクティスをツール化してみた【UnityMasterData】

はじめに

この記事では、私がソーシャルゲーム開発にエンジニアとして、またプランナーとして、個人としても企業としても長年携わってきて、ベストプラクティスと思われるマスターデータ運用について前置きとして紹介させていただきます。また、最後にそのベストプラクティスを個人から企業まで幅広く汎用的に使用できるものとしてツール化したものをご紹介させていただきます。

前置きが長いので、ツール紹介のみを閲覧希望の方はこちらからジャンプしてください。

Unityでのマスターデータ運用に使われるツールについて

Unityを使用したマスターデータ運用において、一次ソースデータとしておそらく下記のようなものがあげられると思います。

  • ScriptableObject
  • Excel
  • Google SpreadSheet

このうちScriptableObjectをマスターデータの手動入稿先として運用している企業はほとんどありませんが、ここ数年でGoogle SpreadSheetを採用し始めた企業も多いのではないでしょうか?
私自身も色々な会社を転々としておりますが、その中でもここ2、3年でGoogle SpreadSheetを使用しているプロダクトに配属されるケースが多くなってきました。

Google SpreadSheetをマスターデータ運用に使用するケース

これはあくまで一例ですが、Google SpreadSheetを使用する場合、下記のようなワークフローになる場合が多いです。

The workflow using Google SpreadSheet (1).png

ここでは、動的に変化するシート内のデータをスタティックなものとして扱うため、また差分情報をGitで確認できるように中間的なデータとしてS3CSVGASでアップロードして、それをCIが専用リポジトリにコミットする流れを取っております。

実際に運用してみての感想

Excelのようにgitで差分を簡単に確認はできませんが、Google SpreadSheetを使用することで同じシートを複数人で同時に入稿作業してもコンフリクトは発生しませんので、当時私もExcelを使用する以上にGoogle SpreadSheetを推奨していた時がありました。また、Google SpreadSheetで入稿したデータはGoogle App Script(GAS)を使用することでCIによる様々な自動化が可能であると考えておりました。(というよりExcelなんてレガシー、モダンなGoogle SpreadSheet使うでしょ?みたいな感覚でした

ですが、Google SpreadSheetを使用したプロダクトを数ヶ月、数年と運用していくうちに上記のメリットを大きく上回るデメリットがあることに気づきました。個人や極小規模のプロダクトならこれらのデメリットを感じないケースもあるかもしれませんが、実際にある程度の規模のプロダクトで運用されてきた方なら以下のようなデメリットを感じた方も多いのではないでしょうか?

  • 誰がどのような変更をしたのかが、過去を遡って特定できない
  • フィルタリングが共有されてしまう(個人用フィルタリングも使いづらい)
  • ブランチ運用に超絶向いていない(GASで無理やりやってもちゃんとしたマージなんてまーじ無理)
  • ちょっとした更新をするにしても時間がかかる(エディターで直ダウンロードしようとしてもパブリッシングデータがいつ更新されているのかがポーリングできない)
  • アカウント管理をちゃんとしてない場合、個人アカウントで作られたシートがその人が退職してアカウントが破棄されると、シートも破棄されて絶望する

などなど、他にもあげるとキリがありませんが、この中でも「ちょっとした更新をするにしても時間がかかる」というデメリットはなんども調整に調整を重ねるようなマスターデータ運用には、非常に不向きであると感じました。そして、これらのデメリットがあまりにも大きいため、この時点でだんだんと

「あれ、Excelくんって割と優秀な子だった?」

という思いが強くなってきました。

Excelをマスターデータ運用に使用するケース

では反対にExcelをマスターデータ運用の一次ソースデータとして使用した場合について、今まで私が経験した中でベストプラクティスと感じたワークフローは以下のようなものでした。

The workflow using Excel.png

S3やマスターデータ専用リポジトリという中間の存在がなくなったため、先ほどのワークフローより幾分かシンプルに見えると思います。実際、Excelはユーザーが直接リポジトリにコミットすることが可能なため、CI側としてはポーリングを容易に実装することができ、ユーザーがわざわざジョブキッカーになる必要は無くなります。

実際に運用してみての感想

ご存知の通り、大きなデメリットとして同じファイルを追加、編集する際に確実に作業者どちらかの方でコンフリクトが発生してしまい、そのコンフリクトの解決が非常に面倒であるというものがあります。また、Google SpreadSheetとは違い、ライセンスを購入する必要もあり、そのままではGitでの差分もバイナリファイル扱いされるため、差分を確認することもできません。

これらのような非常に大きいデメリットを抱えているExcelですが、私はこのデメリットすらも大きく上回るメリットがExcelにはあると感じております。具体的には以下のようなものです。

  • 調整がローカルで全て完結する
  • xlsxファイルであれば、内部がxmlファイルなので、解析ツールが容易に作成できる。また、差分確認などであれば、世の中にたくさんのツールが出回っている
  • 昔からよく使われているツールなので、比較的ほとんどのプランナーが使用できる
  • それぞれのユーザーがGitに直接コミットするので、誰がいつ何をしたのかがすぐにわかる
  • pythonなどでマージツールを作成することができるため、ブランチ運用と相性が良い
  • ローカルにあるファイルなので、好き勝手にフィルタリングできる(コミットしないでねw)

などがあげられます。もちろん技術的コストがかかりますが、マージツールを作成することでブランチ運用できることや、比較的ほとんどのプランナーが使用できるという点から、チーム開発する場合でもGoogle SpreadSheetより優れているのではないか、というのが個人的な意見です。

それぞれのデメリットの解決方法

Excelを使用するにあたって、大きなデメリットがあるという点について前述いたしましたが、今までの私のプロダクトではこれらを克服するために下記のような対策を行うことで、デメリットを比較的抑えることができました。

作業者間で発生するコンフリクトの対策

GitSVNとは違い、ファイル単位でのロックは不可能なため、これらの対策をするには第3者からの干渉が必須と考えました。

その最も良かった対策の一つとして、管理ツールで編集者登録をし、JenkinsからPullRequestのマージをユーザーごとにバリデーションするというものがあげられます。図にすると下記のようなものになります。

The workflow to prevent conflicts at excel files.png

Slackに関して、強制力はありませんが各ユーザーに通知を行うことで、よりコンフリクトを避けられるように設計します。とはいえ、ソースコードと同じで基本的には同じものを追加、編集するという場合には基本的に当事者間での情報の共有は必須と考えます。

デフォルト設定で差分を確認できない

前述している通り、こちらはパブリックに公開されているツールが複数あります。

仕組みとしては単純で、xlsxという形式のExcelであれば基本的にxmlファイルをzipでアーカイブしているものになり、これら公開されている差分確認ツールは一般にそれらExcelファイルの中身を解析して、tsvファイルとして出力するためのツールとなっております。

これらxlsxファイルをtsvファイルとして出力するツールを.gitconfig.gitattributesに設定することで、コンソールではもちろん、SourceTreeのようなGUIツールでもバイナリファイルとしてではなく、tsvファイルとして認識され、差分を出力することが可能です。

結論、どちらを使えば良いのか

長々とExcelを使用した場合と、Google SpreadSheetを使用した場合とで、実際に運用した感想について話させていただきましたが、結論としてどちらを使用するべきかは正直プロダクトや企業、開発するものによって、どちらが良いかは分かれると思います。

例えば、これまでGoogle SpreadSheetを運用してきた経験やツールがすでにあり、GASで作成したツールもたくさんある場合は無理にExcelを使用するべきではないかもしれませんし、アクションゲームのようにキャラクターのパラメータを頻繁に変更しながらバランスを調整するような開発スタイルをとる場合、反映速度が遅いGoogle SpreadSheetを無理に使用するべきではないと考えます。

ですが、私個人的な意見を申すと、もしあなたがまだマスターデータの運用をどうするか迷っている、もしくはまだ何も手をつけていない状態であるのであれば、Excelをマスターデータ運用で使用することをお勧めします

マスターデータ運用ツールの紹介

前置きがかなり長くなりましたが、ここからは私が前述した内容をより汎用的に使用できるようツール化したものの紹介になります。また、ここから紹介する内容はREADMEで公開している内容を詳しく日本語で解説したものになります。英語でのドキュメントになりますが、より簡易的な使用方法については以下のリポジトリを直接参照ください。

https://github.com/tani-shi/unity-master-data

How works UnityMasterData (1).png

特徴

このツールを使用する上で、大きな特徴は以下の通りです。

  • Excelをマスターデータ一次ソースファイルとして使用する
  • DTO/DAO/VOの設計思想を導入している
  • ScriptableObjectをマスターデータアセットとして使用する(AssetBundleとの親和性を高めるため)
  • マスターデータ操作用のスクリプトは全てジェネレーターによって生成される(CIとの親和性を高めるため)
  • Excelファイル内でEnumタイプ定義をすることができる(タイプ定義がコメントの役割をもつ上、コメントとは違い確実にスクリプトでの定義と同一になる)
  • マスターデータアセットのロード処理のプロセスのみをユーザーが定義できる(デモではAddressablesの使用を推奨しております)

導入方法

このツールを使用する前に、以下の手順を行い導入してください。

1. NuGetForUnityのパッケージをプロジェクトに取り込む

こちらから最新のパッケージをダウンロードして、Editorからインポートしてください。
https://github.com/GlitchEnzo/NuGetForUnity/releases

2. ExcelDataReaderのパッケージをインストールする

NuGetを使用して、ExcelDataReaderの以下のパッケージをインストールしてください。

  • ExcelDataReader
  • ExcelDataReader.DataSet

NuGetを使用してパッケージをダウンロードする方法については、こちらで詳しく解説しております。

3. Addressable Assets Systemのパッケージをインストールする

UnityMasterDataのサンプルスクリプトでAddressablesを使用しております。
メニューからWindow/Package ManagerPackageManagerを開いて、Addressablesをインストールしてください。

4. UnityMasterDataのパッケージをプロジェクトに取り込む

こちらから最新のパッケージをダウンロードして、Editorからインポートしてください。
https://github.com/tani-shi/unity-master-data/releases

UnityMasterDataの導入は以上になります。

ワークフロー

このツールを使用する上で、以下のようなワークフローを想定しております。

1. マスターデータをエクセルに入稿する

ここで入稿する場合以下のようなフォーマットである必要があります。

A B...
1 プライマリーキー名 *1 フィールド名 <= 必ず入力してください
2 コメント コメント <= 空でも可
3 Enum定義 *2 <= 空でも可
4 プライマリーキータイプ *1 フィールドタイプ *2 <= 必ず入力してください
5〜 プライマリーキー値 パラメータ値 <= タイプがstringであれば空でも可。それ以外は必ず入力してください
  • *1 プライマリーキー名はidとなり、プライマリーキータイプはintもしくはuintになるのが一般です。
  • *2 Enum定義が空でない場合は、そのカラムのフィールドタイプがそのEnumのタイプ名となり、自動的にMasterDataType.csに定義されます。

サンプルファイルを用意しております。参照ください。

2. マスターデータ操作用のスクリプトを生成・更新する

MasterDataClassGenerator.GenerateAllDataScriptsをあなたのプロジェクトのスクリプトから呼び出してください。この時、以下のようなスクリプトが生成されます。

- {0}/MasterData/DAO/Generated/{1}/{2}DAO.cs
- {0}/MasterData/DTO/Generated/{1}/{2}DTO.cs
- {0}/MasterData/VO/Generated/{1}/{2}VO.cs
- {0}/MasterData/Type/Generated/MasterDataType.cs
- {0}/MasterData/Collection/Generated/MasterDataAccessorObjectCollection.cs
- {0}/MasterData/Editor/Exporter/Generated/{1}Exporter.cs

{0} = 設定したスクリプト生成ルートパス
{1} = エクセルファイル名
{2} = シート名

3. マスターデータをScriptableObjectとして書き出す

MasterDataExporter.Exportをあなたのプロジェクトのスクリプトから呼び出してください。この時、以下のようなアセットが生成されます。

- {0}/MasterData/{1}/{2}.asset

{0} = 設定したアセット書き出しルートパス
{1} = エクセルファイル名
{2} = シート名

基本的なワークフローとしては、ここまでの1.〜3.を繰り返してマスターデータの調整を行っていくような想定をしております。

4. MasterDataManagerBaseを継承して、プロジェクト固有のアセットロード処理を実装したクラスを作成する

ワークフローとして紹介しておりますが、このステップは一度実装すれば基本的に更新する必要はありません。
こちらはサンプルとして、Addressablesを使用してロード処理を実装したサンプルスクリプトです。

5. 4.で作成したマネージャークラスを使いマスターデータを操作する

上で紹介したサンプルマネージャークラスを実装したサンプルスクリプトです。
注意することとして、マスターデータを取得する処理を実装する箇所よりも先に、マスターデータアセットをロードする処理を実装した箇所が先に実行されなければなりません。

最後に

今回紹介させていただいたツールは私が完全に個人的な見解で、今まで経験したマスターデータの運用の中で最も効率的にゲーム開発ができる設計と感じたものをツール化したものになります。もちろんこれが最適解かどうかというとケースバイケースですが、個人で開発をしていてチーム開発のワークフローを経験したことがない方や、これからソーシャルゲーム開発を行っていく上で技術を模索しているような企業の方の何か参考になるようなものになれば幸いです。

また、今回はGoogle SpreadSheetについて運用する上でデメリットばかり触れてしまいましたが、実はこういう使い方するとこっちの方が使いやすいよ!といったような意見があれば、ご指摘いただきたいです。

それでは最後まで閲覧いただき、ありがとうございました。

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

OculusQuest のハンドトラッキングで手の関節情報などを取得する方法

OVRSkelton を利用すると、片手で24箇所の位置(以下の図のキューブの箇所)が取得できます。
lefthand.jpg

前提

Unity 2019.3.0f3
Oculus Integration 12.0

OVRSkelton.cs について

Oculus Integration でハンドトラッキングする時に追加する、OVRHandTrackingPrefab に最初から設定されているプログラムです。このプログラムが持っている Bones(OVRBoneというクラスのリスト)と、各位置を指し示す BoneId を利用することで、指の関節などの情報を取得します。

部位取得の概要

部位 取得箇所数 指定方法の概要
親指 5 付け根側から 0, 1, 2, 3, tip
人差し指 4 付け根側から 1, 2, 3, tip
中指 4 付け根側から 1, 2, 3, tip
薬指 4 付け根側から 1, 2, 3, tip
小指 5 付け根側から 0, 1, 2, 3, tip
手首 2 WristRoot, ForearmStub

部位を指定するBoneIdの詳細

(上の図を見た方が解りやすい)

BoneId 部位の説明
Hand_WristRoot 手の付け根
Hand_ForearmStub 腕でも手に近いところ
Hand_Thumb0 親指の第3関節から手首側に小さな骨があり、その付け根
Hand_Thumb1 親指の第3関節
Hand_Thumb2 親指の第2関節
Hand_Thumb3 親指の第1関節
Hand_ThumbTip 親指の先端
Hand_Index1 人差し指の第3関節
Hand_Index2 人差し指の第2関節
Hand_Index3 人差し指の第1関節
Hand_IndexTip 人差し指の先端
Hand_Middle1 中指の第3関節
Hand_Middle2 中指の第2関節
Hand_Middle3 中指の第1関節
Hand_MiddleTip 中指の先端
Hand_Ring1 薬指の第3関節
Hand_Ring2 薬指の第2関節
Hand_Ring3 薬指の第1関節
Hand_RingTip 薬指の先端
Hand_Pinky0 小指の第3関節から手首側に伸びている骨の付け根
Hand_Pinky1 小指の第3関節
Hand_Pinky2 小指の第2関節
Hand_Pinky3 小指の第1関節
Hand_PinkyTip 小指の先端

外部から値を取得するプログラム

サンプル

using UnityEngine;

public class Hand : MonoBehaviour
{
    [SerializeField] private OVRSkeleton skeleton;

    [SerializeField] private Transform thumb0;
    [SerializeField] private Transform thumb1;
    [SerializeField] private Transform thumb2;
    [SerializeField] private Transform thumb3;
    [SerializeField] private Transform thumbTip;
    [SerializeField] private Transform index1;
    [SerializeField] private Transform index2;
    [SerializeField] private Transform index3;
    [SerializeField] private Transform indexTip;
    [SerializeField] private Transform middle1;
    [SerializeField] private Transform middle2;
    [SerializeField] private Transform middle3;
    [SerializeField] private Transform middleTip;
    [SerializeField] private Transform ring1;
    [SerializeField] private Transform ring2;
    [SerializeField] private Transform ring3;
    [SerializeField] private Transform ringTip;
    [SerializeField] private Transform pinky0;
    [SerializeField] private Transform pinky1;
    [SerializeField] private Transform pinky2;
    [SerializeField] private Transform pinky3;
    [SerializeField] private Transform pinkyTip;

    [SerializeField] private Transform start;
    [SerializeField] private Transform wristRoot;
    [SerializeField] private Transform forearmStub;
    [SerializeField] private Transform maxSkinnable;
    [SerializeField] private Transform end;

    void Update()
    {
        thumb0.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Thumb0].Transform.position;
        thumb1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Thumb1].Transform.position;
        thumb2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Thumb2].Transform.position;
        thumb3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Thumb3].Transform.position;
        thumbTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_ThumbTip].Transform.position;

        index1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Index1].Transform.position;
        index2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Index2].Transform.position;
        index3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Index3].Transform.position;
        indexTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;

        middle1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Middle1].Transform.position;
        middle2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Middle2].Transform.position;
        middle3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Middle3].Transform.position;
        middleTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_MiddleTip].Transform.position;

        ring1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Ring1].Transform.position;
        ring2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Ring2].Transform.position;
        ring3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Ring3].Transform.position;
        ringTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_RingTip].Transform.position;

        pinky0.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Pinky0].Transform.position;
        pinky1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Pinky1].Transform.position;
        pinky2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Pinky2].Transform.position;
        pinky3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Pinky3].Transform.position;
        pinkyTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_PinkyTip].Transform.position;

        wristRoot.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_WristRoot].Transform.position;
        forearmStub.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_ForearmStub].Transform.position;

        start.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Start].Transform.position;
        // maxSkinnable.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_MaxSkinnable].Transform.position;
        end.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_End].Transform.position;
    }
}

補足

  • Hand_Start、Hand_MaxSkinnable、Hand_End はBoneIdとしては使用されておらず、プログラムを読む限り、Enum の中で値を仕切るために使用されているように見える。
    • このプログラムを実行しても、Hand_Start、Hand_Endのキューブは表示されることがない。
    • Hand_MaxSkinnable をコメントアウトしているのは、親指の先端と同じ値が設定されているため。コメントを外すと親指の先端箇所に Hand_MaxSkinnable のキューブが表示される。
  • OVRSkelton の BoneId の enum の中に、 // add new bones here とコメントがあることから、これからまだ何かしらの値を追加していく想定見て取れる。それを見越しての、Hand_Start などなのだと思われる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OculusQuest のハンドトラッキングで指の関節情報などを取得する方法

OVRSkelton を利用すると、片手で24箇所の位置(以下の図の球の箇所)が取得できます。
lefthand.jpg

前提

Unity 2019.3.0f3
Oculus Integration 12.0

OVRSkelton.cs について

Oculus Integration でハンドトラッキングする時に追加する、OVRHandTrackingPrefab に最初から設定されているプログラムです。このプログラムが持っている Bones(OVRBoneというクラスのリスト)と、各位置を指し示す BoneId を利用することで、指の関節などの情報を取得します。

この方の記事がわかりやすいです。(この記事を書く前に知りたかった・・・)
https://qiita.com/divideby_zero/items/4949fadb2c60f810b3aa

部位取得の概要

部位 取得箇所数 指定方法の概要
親指 5 付け根側から 0, 1, 2, 3, tip
人差し指 4 付け根側から 1, 2, 3, tip
中指 4 付け根側から 1, 2, 3, tip
薬指 4 付け根側から 1, 2, 3, tip
小指 5 付け根側から 0, 1, 2, 3, tip
手首 2 WristRoot, ForearmStub

部位を指定するBoneIdの詳細

(上の図を見た方が解りやすい)

BoneId 部位の説明
Hand_WristRoot 手の付け根
Hand_ForearmStub 腕でも手に近いところ
Hand_Thumb0 親指の第3関節から手首側に小さな骨があり、その付け根
Hand_Thumb1 親指の第3関節
Hand_Thumb2 親指の第2関節
Hand_Thumb3 親指の第1関節
Hand_ThumbTip 親指の先端
Hand_Index1 人差し指の第3関節
Hand_Index2 人差し指の第2関節
Hand_Index3 人差し指の第1関節
Hand_IndexTip 人差し指の先端
Hand_Middle1 中指の第3関節
Hand_Middle2 中指の第2関節
Hand_Middle3 中指の第1関節
Hand_MiddleTip 中指の先端
Hand_Ring1 薬指の第3関節
Hand_Ring2 薬指の第2関節
Hand_Ring3 薬指の第1関節
Hand_RingTip 薬指の先端
Hand_Pinky0 小指の第3関節から手首側に伸びている骨の付け根
Hand_Pinky1 小指の第3関節
Hand_Pinky2 小指の第2関節
Hand_Pinky3 小指の第1関節
Hand_PinkyTip 小指の先端

外部から値を取得するプログラム

サンプル

using UnityEngine;

public class Hand : MonoBehaviour
{
    [SerializeField] private OVRSkeleton skeleton;

    [SerializeField] private Transform thumb0;
    [SerializeField] private Transform thumb1;
    [SerializeField] private Transform thumb2;
    [SerializeField] private Transform thumb3;
    [SerializeField] private Transform thumbTip;
    [SerializeField] private Transform index1;
    [SerializeField] private Transform index2;
    [SerializeField] private Transform index3;
    [SerializeField] private Transform indexTip;
    [SerializeField] private Transform middle1;
    [SerializeField] private Transform middle2;
    [SerializeField] private Transform middle3;
    [SerializeField] private Transform middleTip;
    [SerializeField] private Transform ring1;
    [SerializeField] private Transform ring2;
    [SerializeField] private Transform ring3;
    [SerializeField] private Transform ringTip;
    [SerializeField] private Transform pinky0;
    [SerializeField] private Transform pinky1;
    [SerializeField] private Transform pinky2;
    [SerializeField] private Transform pinky3;
    [SerializeField] private Transform pinkyTip;

    [SerializeField] private Transform start;
    [SerializeField] private Transform wristRoot;
    [SerializeField] private Transform forearmStub;
    [SerializeField] private Transform maxSkinnable;
    [SerializeField] private Transform end;

    void Update()
    {
        thumb0.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Thumb0].Transform.position;
        thumb1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Thumb1].Transform.position;
        thumb2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Thumb2].Transform.position;
        thumb3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Thumb3].Transform.position;
        thumbTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_ThumbTip].Transform.position;

        index1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Index1].Transform.position;
        index2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Index2].Transform.position;
        index3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Index3].Transform.position;
        indexTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;

        middle1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Middle1].Transform.position;
        middle2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Middle2].Transform.position;
        middle3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Middle3].Transform.position;
        middleTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_MiddleTip].Transform.position;

        ring1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Ring1].Transform.position;
        ring2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Ring2].Transform.position;
        ring3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Ring3].Transform.position;
        ringTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_RingTip].Transform.position;

        pinky0.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Pinky0].Transform.position;
        pinky1.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Pinky1].Transform.position;
        pinky2.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Pinky2].Transform.position;
        pinky3.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Pinky3].Transform.position;
        pinkyTip.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_PinkyTip].Transform.position;

        wristRoot.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_WristRoot].Transform.position;
        forearmStub.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_ForearmStub].Transform.position;

        start.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_Start].Transform.position;
        // maxSkinnable.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_MaxSkinnable].Transform.position;
        end.position = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_End].Transform.position;
    }
}

補足

  • Hand_Start、Hand_MaxSkinnable、Hand_End はBoneIdとしては使用されておらず、プログラムを読む限り、Enum の中で値を仕切るために使用されているように見える。
    • このプログラムを実行しても、Hand_Start、Hand_Endの球は表示されることがない。
    • Hand_MaxSkinnable をコメントアウトしているのは、親指の先端と同じ値が設定されているため。コメントを外すと親指の先端箇所に Hand_MaxSkinnable の球が表示される。
  • OVRSkelton の BoneId の enum の中に、 // add new bones here とコメントがあることから、これからまだ何かしらの値を追加していく想定見て取れる。それを見越しての、Hand_Start などなのだと思われる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tilemapのブラシ拡張のための属性、CustomGridBrushについて

Unityの2DマップエディターであるTilemap。このTilemapはTile・Brushを拡張することができます。

この投稿では、Brushの拡張で使う「CustomGridBrush」属性について説明します。


CustomGridBrushの利用例を次に示します。

それぞれのコードはUnity公式サンプルの2d-gamedemo-robodashのものです。

[CreateAssetMenu]
[CustomGridBrush(false, true, false, "Level")]
public class LevelBrush : GridBrushBase {
    /* 中略 */
}
[CustomGridBrush(true, false, false, "Tint")]
public class TintBrush : GridBrushBase {
    /* 中略 */
}
[CustomGridBrush(true, true, true, "Default Brush")]
public class DefaultBrushReplacement : GridBrush 
{
    /* 中略 */
}

このように、CustomGridBrushGridBrushクラスを継承したクラスに付与する属性です。


CustomGridBrushbool,bool,bool,string型のパラメータをとります。

それぞれ名前付き呼び出しで書くと次のようになります。

[CustomGridBrush(hideAssetInstances: true, hideDefaultInstance: false, defaultBrush: true, defaultName: "Default Brush")]
public class DefaultBrushReplacement : GridBrush 
{
    /* 中略 */
}

パラメータはそれぞれ最初から

  • boolhideAssetInstances
  • boolhideDefaultInstance
  • booldefaultBrush
  • stringdefaultName

です。


それぞれのパラメータを理解するためには、3種類のBrushについて理解する必要があります。

Brushには

  • アセット(ScriptableObject)を作るBursh
  • アセット(ScriptableObject)を作らないBrush
  • デフォルトを書き換えるのBrush

があります。

「アセット(ScriptableObject)を作るBursh」の利用例としては、特定のプレファブを設置するBrushがあげられます。このBrushは「同じ機能を持つBrushをアセットとして複数作って、それぞれ違うプレファブを設置できるようにする」ということができます。この種類のBrushは「同じ機能を持っているけれど、別の設定値を持っているBrush」を個別にアセットとして生成します。LevelBrush、TeleportBrushやLaserBrushなど「2d-gamedemo-robodash」プロジェクトの多くのBrushが「アセットを作るBrush」です。プロジェクト中にScriptableObjectとして、Brushが存在することに注目してください。

「アセット(ScriptableObject)を作らないBrush」の利用例としては、線を引くBrush、座標情報を表示するBrsuhなどの機能を提供するBrushがあります。「線を引く機能」はバリエーションを作る必要がありません。このようにバリエーションを作る必要がないBrushは、「アセット(ScriptableObject)を作らないBrush」となります。2d-gamedemo-robodash中のTintBrush、そして2d-extras中の、LineBrushやCordinateBrushが「アセット(ScriptableObject)を作らないBrush」です。

「デフォルトを書き換えるのBrush」は、Unityが提供するデフォルトのBrushを置き換えるBrushです。2d-gamedemo-robodashでは、デフォルトのBrushを置き換えています。


それぞれのパラメータの意味を紹介します。

  • hideAssetInstancesがtrueだと、プロジェクト中のアセット(ScriptableObject)として作られたBrushは、Tile Palette Windowから選べなくなります。その型のBrushをアセットとして作る意味がなくなります。
  • hideDefaultInstanceがfalseだと、プロジェクト中にアセットとして作られたその型のBrushがなくてもデフォルトのBrushが作られ、Tile Palette Windowから選べます。trueだとTile Palette Windowから選べばせん。
  • defaultBrushがtrueだと、Unityが提供する標準のBrushを置き換えます。プロジェクトの中で一つのBrushだけtrueにすべきですね。
  • defaultNameは、その型のBrushのデフォルトBrushの名前です。これを設定しない場合、型名からBrush名が作られます。

先のコードにあげたパラメータを確認・解説します。

まずはDefaultBrushReplacement

[CreateAssetMenu]
[CustomGridBrush(false, true, false, "Level")]
public class LevelBrush : GridBrushBase {
    /* 中略 */
}

これはアセット(ScriptableObject)を作るBurshです。

  • hideAssetInstancesはfalseです。プロジェクト中にアセット(ScriptableObject)を作りたいので、falseにする必要がありますね。
  • hideDefaultInstanceはtrueです。デフォルトのBrush入らないのでtrueにした方がいいですね。
  • defaultBrushはfalseです。Unityが提供する標準のBrushを置き換えないのでfalseですね。
  • defaultNameは、"Level"になっていますが、DefaultのBrushは無効なので実際は意味がありませんね。

次にTintBrush

これはアセット(ScriptableObject)を作らないでデフォルトのBrushのみを使うBrushです。

[CustomGridBrush(true, false, false, "Tint")]
public class TintBrush : GridBrushBase {
    /* 中略 */
}
  • hideAssetInstancesはtrueです。プロジェクト中にアセット(ScriptableObject)を作らないので、trueにする必要がありますね。
  • hideDefaultInstanceはfalseです。デフォルトのBrushが必要なのでfalseにしなくてはいけませんね。
  • defaultBrushはfalseです。Unityが提供する標準のBrushを置き換えないのでfalseですね。
  • defaultNameは、"Tint"になっています。

最後にDefaultBrushReplacement

[CustomGridBrush(true, false, false, "Tint")]
public class TintBrush : GridBrushBase {
    /* 中略 */
}
}

Unityが提供する標準のBrushを置き換えるBrushです。プロジェクト中のアセット(ScriptableObject)は作りません。

  • hideAssetInstancesはtrueです。プロジェクト中のアセット(ScriptableObject)を作らないので、trueでいいですね。
  • hideDefaultInstanceはtrueです。一見これはfalseでもよさそうですね。hideDefaultInstanceをfalseにしdefaultBrushをtrueにすると、そう設定したBrushがTile Palette Windowで二つ重複して出てきてしまいます。
  • defaultBrushはtrueです。Unityが提供する標準のBrushを置き換えたいのでtrueですね。
  • defaultNameは、"Default Brush"

この投稿では、Tilemapのブラシ拡張のための属性、CustomGridBrushについて紹介しました。

ガンガンBrush、拡張していきましょう!

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

【Unity】ProBuilderWindowに自作アクションを追加する

初投稿です。
よろしくお願いします。
はてブロも最近始めたのですが、コードが多くなりそうな記事やプログラミングについての記事はqiitaに投稿しようと思います。

はてなブログ
https://blog.hatena.ne.jp/Dash7s/dash7s.hatenablog.com/

経緯

ブログのほうでProBuilderをいじって遊んでいたのですが、気が付くと変更がすべて消えていました。
原因は全く分からないのですが、Packagesフォルダ以下のスクリプトを直接編集したのが原因かもしれません。
なので、ProBuilderのAPIを使って、自作アクションをメニューに追加しようと思います。
2020_01_03_5.jpg

環境

Unity 2019.2.5.f1
ProBuilder 4.0.5

手順

1.AssemblyDefinitionの設定(ProBuilder 4.1.0以降は不要)

未確認ですが、ProBuilder4.1.0以降は自動参照されるらしいのでこの手順は不要なようです。
https://forum.unity.com/threads/unable-to-access-unityeditor-probuilder-namespace-in-my-own-editor-scripts.707900/
https://docs.unity3d.com/Packages/com.unity.probuilder@4.1/changelog/CHANGELOG.html

Editorフォルダ内にスクリプトを作成して、同じフォルダにAssemblyDefinitionを作成。
次のように設定する。
2020_01_03_1.jpg

2.スクリプトを編集する

追加したスクリプトを編集して、ウィンドウにアクションを追加します。

[ProBuilderMenuAction]
public class CreateMyShape : MenuAction
{
    //オブジェクトのグループを設定する
    //ウィンドウに表示されるアイコンの色や表示位置が変わります
    public override ToolbarGroup group
    {
        get => ToolbarGroup.Tool;
    }

    //ProBuilderウィンドウがアイコンモードの時に表示される画像
    //nullなら、アイコンモード時は非表示
    public override Texture2D icon
    {
        get => Resources.Load<Texture2D>("Images/ProBuilderEditor/CreateMyShape");
    }

    //ツールチップ(カーソルを置くと表示されるやつ)の設定
    public override TooltipContent tooltip
    {
        get => new TooltipContent("Title", "Summary", "Shortcut"); 
    }

    //選択時に呼ばれる
    public override ActionResult DoAction()
    {
        return new ActionResult(ActionResult.Status.Success, "Success");
    }
}

3.完成

こんな感じになります。
2020_01_03_2.jpg
DoActionを実装していないので、クリックしても何も起きません。
かなり個人的な使い方をするので、実装はブログの方でやります。

MenuActionの使えそうな機能

enable

特定の条件を満たす時のみ使用可能にできる。

public override bool enabled
{
    get => 0 < count;
}

disabledIcon

無効時にアイコンモードで表示される画像
iconを設定していると、デフォルトで、アイコン名+_disabledを検索してくれるらしいのですが、どうも上手くいきませんでした。

protected override Texture2D disabledIcon
{
   get => Resources.Load<Texture2D>("Images/ProBuilderEditor/CreateMyShape_disabled");
}

hidden

アクションを表示するかどうか

public override bool hidden
{
    get => true;
}

menuTitle

テキストモード時に表示されるメニューのタイトルを変更する。
デフォルトではツールチップのタイトルが使用されます。

public override string menuTitle
{
    get => "MenuTitle";
}

optionsMenuState

代替アクション(altを押しながらクリック)が存在するかどうか。
このプラスボタンと歯車みたいなやつが追加されます。
2020_01_03_4.jpg 2020_01_03_3.jpg

protected override MenuActionState optionsMenuState
{
    get => MenuActionState.VisibleAndEnabled;
}

toolbarPriority

ツールバーのどの位置に表示するか。

public override int toolbarPriority
{
    get => 0;
}

validSelectModes

どのモードの時に表示するかを指定する。
hiddenが設定されていたらそっちが優先される。

public override SelectMode validSelectModes
{
    get => SelectMode.Edge;
}

DoAlternateAction

代替アクションが実行されたときに呼ばれる。
オーバーライドしない場合は、OnSettingGUIで実装するウィンドウが自動生成される。

protected override void DoAlternateAction()
{
    Debug.Log("AlternateAction");
}

OnSettingsGUI

デフォルトの代替アクションで生成されたウィンドウを作成する。

protected override void OnSettingsGUI()
{
    EditorGUILayout.LabelField("OnSettingsGUI");
}

OnSettingsEnable

代替アクションでウィンドウが生成されたときに呼ばれる。

protected override void OnSettingsEnable()
{
    Debug.Log("OnSettingsEnable");
}

OnSettingsDisable

代替アクションで生成されたウィンドウが破棄されたときに呼ばれる。

protected override void OnSettingsDisable()
{
    Debug.Log("OnSettingsDisable");
}

参考

https://docs.unity3d.com/Packages/com.unity.probuilder@4.0/api/UnityEditor.ProBuilder.html
https://github.com/Unity-Technologies/ProBuilder-API-Examples
https://forum.unity.com/threads/probuilder-in-game-api-changes.624184

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

【Unity(C#)】ハンドトラッキングで簡易版VRお絵かきアプリ

ハンドトラッキング

OculusQuestにとんでもないテクノロジーがやってきました。(今更)
まだベータ版みたいですが、ハンドトラッキングがアップデートで実装されました。(今更)

お勉強がてら中身をいじってみようと思います。
今回は下記記事を参考に、簡易版お絵かきアプリを実装してみようと思います。

【参考リンク】:OculusQuest ハンドトラッキングSDKから、指Boneの情報を取得し分析する

デモ

なかなかのクオリティですがやりたいことができました。
DrawDemo.gif

実装方法としては、
人差し指を完全に伸ばした状態のときにTrailRendererEmittingtrueに、
それ以外の時はfalseにしているだけです。

コード

using UnityEngine;
/// <summary>
/// 適当にオブジェクト作ってアタッチ
/// </summary>
[RequireComponent(typeof(TrailRenderer))]
public class DrawFunction : MonoBehaviour
{
    [SerializeField]
    OVRHand m_oVRHand;

    [SerializeField]
    OVRSkeleton m_ovrSkeleton;

    [SerializeField]
    OVRHand.HandFinger m_handFingerType;

    TrailRenderer m_tr;

    void Reset()
    {
        m_tr = this.gameObject.GetComponent<TrailRenderer>();
        m_tr.time = Mathf.Infinity;
        m_tr.widthMultiplier = 0.01f;
        m_tr.minVertexDistance = 0.01f;
    }

    void Start()
    {
        m_tr = this.gameObject.GetComponent<TrailRenderer>();
    }

    void Update()
    {
        Vector3 indexTipPos = m_ovrSkeleton.Bones[(int)OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;
        this.gameObject.transform.position = indexTipPos;

        if (m_oVRHand.GetFingerPinchStrength(m_handFingerType) == 0)
        {
            m_tr.emitting = true;
        }
        else
        {
            m_tr.emitting = false;
        }
    }
}

TrailRenderer

Unity様が用意した神コンポーネントのおかげでお絵かき機能自体は
ノーコーディングで実装できています。

細かい設定はResetメソッドの中で行っています。
RequireComponentで勝手に引っ付いてくるようにして、
Resetで初期設定を行う王道スタイルです。

    TrailRenderer m_tr;

    void Reset()
    {
        m_tr = this.gameObject.GetComponent<TrailRenderer>();
        m_tr.time = Mathf.Infinity; //消失時間の設定 Infで無限に存在(消失しない)
        m_tr.widthMultiplier = 0.01f; //線の太さ
        m_tr.minVertexDistance = 0.01f; //頂点間の距離 曲線の滑らかさに起因
    }

BoneId

お絵かきするためのTrailRendererの位置を人差し指の指先に合わせます。
先述の参考リンクにもあるように、下記でそれぞれの関節の位置を取得できます。

人差し指の先端場合
Vector3 indexTipPos = m_ovrSkeleton.Bones[(int)OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;

Editor上でPlayModeにした場合(LINKなど無しで)、
ArgumentOutOfRangeExceptionが出ます。気にせずビルドしたらいけました。

GetFingerPinchStrength

今回は指を伸ばした状態でのみ、お絵かきを可能にしました。
最初はGetFingerIsPinchingで引数にIndexを指定していましたが、
指を曲げている、曲げていない の判定が少し厳しかったので断念しました。

なので、指を完全に伸ばした状態(GetFingerPinchStrength == 0)を判定に用いることにしました。

思いのほかうまくいったので今回のお遊びはここまでとしました。

その他詰まった箇所

ビルド後立ち上げたアプリで"ハンドトラッキング未対応"みたいなのが出る

画像の箇所をHands Only or Controllers And Handsに直せばOKです。
HandOnly.PNG

ビルドできない

Android SDKなどのエラーが出てたので
Unityの再インストール(Android SDKなども合わせて)で直りました。
LTSの最新版入れました。

まとめ

今回は超簡易版として作りましたので、
既存のコンポーネントであるTraiRendererを使用しています。

色を変えたりは簡単に実装できそうですが、
消しゴム機能を実装する際にTraiRendererで可能なのか?
というのが懸念の一つです。

消しゴム機能で好きな箇所だけ自由に消したい...となると
お絵かき機能自体を自前で用意しないと厳しいかもしれません。

何か良い方法あればアドバイスください。

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

【Unity(C#)】ハンドトラッキングで簡単版VRお絵かきアプリ

ハンドトラッキング

OculusQuestにとんでもないテクノロジーがやってきました。(今更)
まだベータ版みたいですが、ハンドトラッキングがアップデートで実装されました。(今更)

お勉強がてら中身をいじってみようと思います。
今回は下記記事を参考に、簡易版お絵かきアプリを実装してみようと思います。

【参考リンク】:OculusQuest ハンドトラッキングSDKから、指Boneの情報を取得し分析する

デモ

なかなかのクオリティですがやりたいことができました。
DrawDemo.gif

実装方法としては、
人差し指を完全に伸ばした状態のときにTrailRendererEmittingtrueに、
それ以外の時はfalseにしているだけです。

コード

using UnityEngine;
/// <summary>
/// 適当にオブジェクト作ってアタッチ
/// </summary>
[RequireComponent(typeof(TrailRenderer))]
public class DrawFunction : MonoBehaviour
{
    [SerializeField]
    OVRHand m_oVRHand;

    [SerializeField]
    OVRSkeleton m_ovrSkeleton;

    [SerializeField]
    OVRHand.HandFinger m_handFingerType;

    TrailRenderer m_tr;

    void Reset()
    {
        m_tr = this.gameObject.GetComponent<TrailRenderer>();
        m_tr.time = Mathf.Infinity;
        m_tr.widthMultiplier = 0.01f;
        m_tr.minVertexDistance = 0.01f;
    }

    void Start()
    {
        m_tr = this.gameObject.GetComponent<TrailRenderer>();
    }

    void Update()
    {
        Vector3 indexTipPos = m_ovrSkeleton.Bones[(int)OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;
        this.gameObject.transform.position = indexTipPos;

        if (m_oVRHand.GetFingerPinchStrength(m_handFingerType) == 0)
        {
            m_tr.emitting = true;
        }
        else
        {
            m_tr.emitting = false;
        }
    }
}

TrailRenderer

Unity様が用意した神コンポーネントのおかげでお絵かき機能自体は
ノーコーディングで実装できています。

細かい設定はResetメソッドの中で行っています。
RequireComponentで勝手に引っ付いてくるようにして、
Resetで初期設定を行う王道スタイルです。

    TrailRenderer m_tr;

    void Reset()
    {
        m_tr = this.gameObject.GetComponent<TrailRenderer>();
        m_tr.time = Mathf.Infinity; //消失時間の設定 Infで無限に存在(消失しない)
        m_tr.widthMultiplier = 0.01f; //線の太さ
        m_tr.minVertexDistance = 0.01f; //頂点間の距離 曲線の滑らかさに起因
    }

BoneId

お絵かきするためのTrailRendererの位置を人差し指の指先に合わせます。
先述の参考リンクにもあるように、下記でそれぞれの関節の位置を取得できます。

人差し指の先端場合
Vector3 indexTipPos = m_ovrSkeleton.Bones[(int)OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;

Editor上でPlayModeにした場合(LINKなど無しで)、
ArgumentOutOfRangeExceptionが出ます。気にせずビルドしたらいけました。

GetFingerPinchStrength

今回は指を伸ばした状態でのみ、お絵かきを可能にしました。
最初はGetFingerIsPinchingで引数にIndexを指定していましたが、
指を曲げている、曲げていない の判定が少し厳しかったので断念しました。

なので、指を完全に伸ばした状態(GetFingerPinchStrength == 0)を判定に用いることにしました。

思いのほかうまくいったので今回のお遊びはここまでとしました。

その他詰まった箇所

ビルド後立ち上げたアプリで"ハンドトラッキング未対応"みたいなのが出る

画像の箇所をHands Only or Controllers And Handsに直せばOKです。
HandOnly.PNG

ビルドできない

Android SDKなどのエラーが出てたので
Unityの再インストール(Android SDKなども合わせて)で直りました。
LTSの最新版入れました。

まとめ

今回は超簡易版として作りましたので、
既存のコンポーネントであるTraiRendererを使用しています。

色を変えたりは簡単に実装できそうですが、
消しゴム機能を実装する際にTraiRendererで可能なのか?
というのが懸念の一つです。

消しゴム機能で好きな箇所だけ自由に消したい...となると
お絵かき機能自体を自前で用意しないと厳しいかもしれません。

何か良い方法あればアドバイスください。

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