- 投稿日:2020-09-28T23:31:38+09:00
VRCSDK3 でバーチャル早着替え with VRoid
みなさん、VRChat 楽しんでいますか? 私はとても楽しんでいます。
さて、先日 VRCSDK3-AVATAR がリリースされました。その特徴は、なんといってもカスタマイズ性の高さです。もとよりプレイヤー自身がアバターやワールドを制作することが VRChat の醍醐味ではありますが、 その長所がより突き詰められた形です。
本記事では、従前のマテリアルアニメーションとアクションメニューのカスタマイズを組み合わせることで、アバターを切り替えることなく衣装をチェンジするバーチャル早着替えを実装します。
STEP0: VRM 素材の準備
VRoid で普通にアバターを作成します。
次に、着替える衣装を用意します。着替える前の服と後の服はモデルの切り替えではなく、テスクチャのレイヤー切り替えで表現してください。今回はマテリアル(=テクスチャ+シェーダ)を差し替えることで早着替えを行うためです。1
衣装が定まったら、着替え前・後で2つの VRM ファイルを出力してください。VRoid での作業は終了です。
STEP1: Unity プロジェクトへのインポート
Unity プロジェクトに VRCSDK3-AVATAR と VRM Converter for VRChat をインポートします。
2つの VRM ファイルをインポートします。続いて、VRM Converter 所定の手順に従い、着替え前のモデルだけ VRChat 用に変換します。
シーン中にある VRChat 用モデルを複製します。2
STEP3: アニメーションの作成
Animationウィンドウ(タブ)で [Create] をクリックし、新しいアニメーションクリップを作成します。名前は何でもよいですが、ここでは
kigae
としました。Animationウィンドウ(タブ)の左上にある記録ボタン(赤丸)を押し、アニメーションの記録を開始します。
足元に複製したモデルが埋まっているので、ヒエラルキーから [Body] を選択しインスペクタを開きます。[Skined Mesh Renderer]-[Materials]で任意のマテリアルを着替え後のマテリアルに差し替えます。ワンピースの場合はトップスのみなので、
F00_002_01_Tops_01_CLOTH
を変更すれば OK です。変更ができたら、再度記録ボタンを押してアニメーションの作成を終了します。複製したモデルは削除しておきましょう。
STEP4: FX の変更
FX はモデルの状態遷移図のようなものです。VRM Converter で変換したモデルでは、あらかじめカスタマイズされた FX が作成されています。
モデルのインスペクタで [VRC Avatar Descriptor]-[Playable Layers]-[FX] に設定されている FX をダブルクリックで開きましょう。
[Parameters] に int 型変数
Kigae
を追加します。この変数はアクションメニューとの連携に利用します。アクションメニューでの操作が変数に反映されるので、FX ではKigae
の変化で服装が変化するようにしましょう。FX は複数の状態遷移図をレイヤーで重ねたような構造になっています。[Layers] タブで
Kigae
レイヤーを追加し、オプションで Weight を 1 に変更します。デフォルトでは Weight が 0 ですが、そのままではモデルにアニメーションが反映されません。状態遷移を作図していきます。右クリックで [Create State]-[Empty] から新たな状態を、状態を右クリックし [Make Transition] から新たな遷移を作成できます。下図のように「着替え前の状態」と「着替え後の状態」を作り、「Entry」から「Exit」がつながるようにしてください。
そして、「着替え後の状態」の [Motion] に先ほど作成したアニメーションを設定します。
Transition を選択し、遷移の条件を設定します。[Conditions] で
Kigae equals 0
で「着替え前の状態」に、Kigae equals 1
で「着替え後の状態」へ遷移するようにします。これで FX の設定は完了です。
STEP4: アクションメニュー の変更
最後に、設定したアニメーションをアクションメニューから呼び出せるようにしましょう。インスペクタで [VRC Avatar Descriptor]-[Expressions]-[VRCExpressionParameters] を開きます。
ここで、先ほど設定した変数
Kigae
との紐付けを行います。変数名と型を設定します。[VRC Avatar Descriptor]-[Expressions]-[VRCExpressionsMenu] を開きます。
[Add Components] からアクションメニューの項目を追加します。
[Parameter]で
Kigae, Int
を選択し、[Value] を1
にします。[Type] で
toggle
を指定すると、次にボタンを押すまで着替えっぱなしになります。ビルド&テストしてみましょう。
アクションメニューに項目が追加され、早着替えを行うことができました!
- 投稿日:2020-09-28T23:12:09+09:00
【Unity】ボタンが押せないのは大体RaycastTargetの設定ミス
UIが反応しない…
上の画像のようにボタンのUIをしっかり設定しているのに、なぜか押せない時ってありますよね?
そんな時は大体RaycastTarget
の設定ミスです。RaycastTargetとは
まず、
Raycast
というのは指定した場所からRay(光線)
を放ち、光線と接触したオブジェクトの情報を取得する機能になります。
銃を撃って敵を攻撃する時に使用したり、本記事のテーマであるUIの情報を取得したりと様々な場面で使うことのできる機能です。では
RaycastTarget
とは何か?
もうお分かりだと思いますが、その名の通りRaycast
のTarget(対象)とするか否かを設定するパラメータになります。
これをtrue
にするとRaycast
のTarget(対象)となるので、情報を取得することができるということです。ボタンが押せない理由
本記事最初の画像のボタンが押せなかった理由は、以下の二つが同時に発生している時になります。
- 「Helloテキスト」がボタンより手前にある
- 「Helloテキスト」の
RaycastTarget
がtrue
手前かつ
RaycastTarget
がtrue
になっていると、背後のオブジェクトまでRay
が通らないのでボタンが反応しない現象が起きるのです。
テキストボックスは見かけを整えるために、実際に見えてる範囲以上の大きさを持っている場合があり、ボタンと重なってしまうことが多いので要注意です。
見かけよりでかいテキスト解決策
基本的にボタン以外は
RaycastTarget
をfalse
にすることで解決します。
新しくUIを作成する時は注意してみてください。
それでも反応しない場合はボタンのOnClick
が設定されていないか、シーン内にEventSystemがないかだと思われます。まとめ
Raycast
はRay(光線)
を放ち、光線と接触したオブジェクトの情報を取得する機能- 基本的にボタン以外は
RaycastTarget
をfalse
にしよう- それでも反応しない場合は
OnClick
を設定しているか確認しよう
- 投稿日:2020-09-28T19:29:12+09:00
HoloLens 2 QRコードトラッキング (追跡) サンプルを試してみる 【 Unity 】
Overview
HoloLens 2 からサポートされている QR Code トラッキング ( HoloLens 1st gen 非対応 ) に関して調査する機会があったので、調べた内容をまとめておきます。
Support Devices
機能 HoloLens (1st gen) HoloLens 2 Immersive Headset (没入型HMD) QRコードの検出 × 〇 〇 没入型 Windows Mixed Reality (WMR) ヘッドセット と デスクトップPC を使った QRコード(追跡)トラッキングは、 Windows 10 Version 2004 以降 で利用することができます。
Microsoft.MixedReality.QRCodeWatcher.IsSupported() API を使って、使用デバイスでQRコード(追跡)トラッキングが可能かどうか判定することができます。
Detecting QR codes in Unity / QRコードの検出
QRコード検出を行うためには、
webcam
capability が必要です。※これは Unity もしくは Visual Studio で設定する必要があります。【 メリット 】
QRコードの検出はデバイスの環境認識カメラ ( HoloLens 2 の場合、4つの環境認識カメラ ) で行われています。これに伴って、より広範囲の視野角 ( FOV ) で検出可能となり、 ( 写真/動画撮影用カメラを使用時と比べて ) バッテリーの寿命を延ばすことができます。
QRコード検出APIは Unity上でMRTKに依存することなく使用することも可能です。
これを実現するためには、NuGet for Unity を使って、対象の NuGet パッケージをインストールしなければいけません。QR code detection ( QRコード検出 ) のための Nuget パッケージはこちらからダウンロードすることができます。【 デメリット 】
デバイス左右に各2つ、計4つの環境認識カメラを使用してQRコードを検出するため、QRコードのサイズがある程度大きくないと認識精度が上がらないというデメリットもあります。
ホロラボ CEO の中村薫さんも Twitter で言及されていたので、補足情報として引用させていただきます。ありがとうございます。
HoloLens 2のQRトラッキング、環境認識カメラを使うのでフレームレートに影響をおよぼさない、QRコードのコード自体を検出してるので画像での認識のように似た画像で誤検出しない、とかがメリット。
— 中村 薫 (@kaorun55) September 27, 2020
デメリットは、左右で魚眼の環境認識カメラを使ってるのでマーカーにある程度のサイズが必要。公式サンプル
公式ドキュメント上に MRTKを使った Unity 向けのサンプル ( GitHub ) が公開されていました。
GitHub ページ : QRTracking/SampleQRCodes
今回はこちらのサンプルを動作させる手順についてご紹介したいと思います。
概要 / Overview
QRコードを検出した際、QRコードの上に白い四角のCGが表示されます。
その他にQRコードから読み取った関連情報も表示されます。
- GUID ( Globally Unique Identifier : グローバル識別子 )
- Physical size
- Timestamp
- decode data
Microsoft HoloLens 2
— 堀尾風仁 Futo Horio (@Futo_Horio) September 27, 2020
QR Code 検出サンプルを
試してみた!
比較的広い範囲トラッキングできるのは、
写真/動画撮影用カメラではなく、
環境認識用のカメラを使用しているため。
( FOV 拡大 + バッテリー寿命も延びた )
?公式ドキュメントhttps://t.co/lPeUHusDK2#HoloLens2 #QRCode pic.twitter.com/4MccVuop0G検証環境
開発用PC ( Windows 10 Pro / ビルド番号 : 19041.207 )
- Unity 2019.4.11f1
- Visual Studio 2019 ( 16.6.2 )
- Windows SDK 10.018362.0
Microsoft HoloLens 2 ( OSビルド番号 : 10.0.19041.1106 )
手順解説
1. GitHub から サンプルプロジェクト を Clone します。
git clone https://github.com/chgatla-microsoft/QRTracking.git2. Unity Hub で対象プロジェクトを追加 ( ADD ) します。
2020年9月28日現在、
Unity 2019.4.0f1
を使用したプロジェクトファイルとなっています。本記事では、Unity 2019.4.11f1 (LTS) で代替し、検証を進めることとします。3. 対象プロジェクトを Unity で開きます。
QRTracking > SampleQRCodes ディレクトリ
4. 対象シーン ( QRCodesSample.unity ) を開きます。
Assets > Scenes > QRCodesSample.unity
以下、MRTK 基本コンポーネントの他に
Axis
,QRCodesManager
というGameObject
が
ヒエラルキー内に存在していることを確認できると思います。
Axis
: QRコードを検出した際に表示する3軸 (x,y,z) のプレハブ
QRCodesManager
: QR Code を検出するためのスクリプト (2つ) がアタッチされたゲームオブジェクト
クラス名 概要 QRCodesManager QR Code のトラッキング状況などを管理するクラス
Auto Start QR Tracking チェックボックスで
QRコードトラッキング 自動開始有無を指定できます。QRCodesVisualizer トラッキング済みの QR Code を可視化するためのクラス 5. ビルドプラットフォームを UWP に切り替えます。
ショートカット : [ Ctrl + Shift + B ] で Build Settings を開きます。
Platform [Universal Windows Platform
] を選択し、Switch Platform
ボタンを押下します。ターゲットプラットフォーム : UWP でビルドを実行します。
※ Scenes In Build リスト内で対象シーンにチェックが入っていることを確認してください。
Unity で Export したソリューションファイル ( .sln ) を Visual Studio 2019 で開きます。
以上でサンプルを試す手順は完了です。
ベストプラクティス
HoloLens 2 で QR Code を扱う際の注意点、ベストプラクティスの抄訳を以下まとめておきます。英語を読める方は公式ドキュメントをお読みになられることをお勧め致します。
1. QRコード周りのスペース / Quit zones around QR Codes
QRコードを正しく読み取るためには、QRコード4つの側面全てで余白が必要となります。
余白 には印刷された他のコンテンツが含まれてはならず、QRコード内の (一番小さい) 黒い四角4つ分の広さ、もしくはそれ以上の大きさが求められます。
「QR Code.com」 より画像を引用
( 詳しくはQRコードの仕様は QR Codes.com をご参照ください。)
2. 照明 と 背景 / Lighting and backdrop
QRコードの検出の精度は、様々な照明や背景に影響を受けやすい性質があります。
特に明るい照明の場所 では、灰色の背景に黒色のQRコードを印刷するようにしてください。
それ以外の場合は、白色の背景に黒色のQRコードを印刷してください。QRコードを配置する環境が特に暗い場合、検出率が低い場合 は、灰色の背景に黒色のQRコードを試してください。( QRコードの検出率が低い場合も同様 ) QRコードを配置する場所が比較的明るい場合は、通常の白背景に黒色のQRコードで問題無く動作するはずです。
3. QRコードのサイズ / Size of QR codes
Windows Mixed Reality (WMR) デバイス では、1辺が5cm未満の QRコードを検出できません。
QRコードの一辺の長さが 5cm ~ 10cm の場合、QRコードを検出するためにかなり近づく必要があります。またこの大きさのQRコードを認識するためには、より長い時間が必要となります。QRコードを検出する正確な時間は、QRコードのサイズだけでなく、QRコードと (デバイスを装着した) ユーザーの距離によっても異なります。QRコードに近づくと、QRコードサイズの問題を解消することができます。
4. QRコード読み取り時の距離と角度 / Distance and angular position from the QR code
( QRコード認識に使用されている ) 環境認識カメラでは、特定のレベルの詳細しか検出することができません。QRコードの1辺の長さが10cm未満の非常に小さいQRコードの場合、QRコードにかなり近づく必要があります。1辺が10cm ~ 25cm の QRコード (バージョン1) を使用した場合、最小検出距離は 15cm ~ 50cm の範囲となります。
QRコードのサイズと検出距離は直線的に増加していきます。
QRコード検出は、±45度の範囲で動作が保証されています。これはQRコードを検出するために必要となる適切な解像度を確保するためです。
5. QRコードを含むロゴ / QR codes with logos
ロゴの中に含まれるQRコードは検出テストが行われておらず、現在サポート対象外 となっています。
6. QRコードデータの管理 / Managing QR code data
Windows Mixed Reality (WMR) デバイスはドライバー内のシステムレベルでQRコードを認識します。デバイスを再起動した際には、検出したQRコードは無くなり、次回以降新しいオブジェクトとして認識されます。
アプリで特定のタイムスタンプより古いQRコードを無視する構成にすることを推奨します。
現在、この QR API ではQRコード読取履歴の削除はサポートされていません。
7. QRコードの配置場所 / QR code placement in a space
QRコードを配置する場所と方法についての推奨事項は、公式ドキュメント「Environment considerations for HoloLens」をご覧ください。
Reference
- 投稿日:2020-09-28T19:25:46+09:00
ARFoundationでTimelineを使うには、、
初心者目線!!
はじめに自己紹介。。
2020年の4月にたき工房という会社にエンジニアとして新卒入社しました。主に、touchdesignerとかunityとか使って、インタラクティブコンテンツやAR,VRなどのプロトタイプを制作しています。こんなん作りました。
https://lab.taki.co.jp/gaikan/今は何もわからないことだらけなので、この記事もなるべく優しく分かりやすいよう書いているつもりです。難しい部分はカット!というか別のリンク貼ります。
ARFoundationでTimelineを使う
ARFoundationとは
から入りますがAR Foundationとは、Unityが作っているARアプリケーション開発用のパッケージです。
ARに関する共通な基本機能をひとまとめにし、各プラットフォーム向けARアプリ開発を最小限の変更で行う ことを目的としています。てな感じで要は、unityでARアプリやゲームを開発する際には絶対必要てことです。
そしてTimelineですが
こんなやつ
Learn more about Timeline, our new powerful visual tool 4 creating cinematic content, from the #UniteEurope keynote https://t.co/D0GVcTtPWB pic.twitter.com/ORIHAv0cUf
— Unity (@unity3d) June 28, 2017
animationのclipをドラックアンドドロップで簡単に演出できる機能です。
adobeのpremiere proとかafter effectsに近い感じでやりやすいなと思っていたが、、、Timelineだけどコードは書かなければならない
コードは書かないのが取り柄なのに書かないといけないんかい
という矛盾。。。
大丈夫、概念自体は少し難しいですが、コード自体はそこまで難しくありません。
手順は主に4つに分かれます。1.ARの開発準備する
2.Timelineを作る
3.演出のコードを書く
4.コードをTimelineに実装
5.実機ビルド開発環境
macOS Catalina:バージョン10.15.5
unity:2020.1.6f.1
iphone11:ios14
xcode:121.ARの開発準備をする
projectを作成したら
まず、main cameraを削除し、
window → package managerを開き、ARFoundationをinstallします。
もしARFoundationが出てこない場合は、Packages:をUnity Registryにしてみてください。ヒエラルキー上で右クリック → XR → AR Session Origin と AR Session を追加
AR Session OriginにAdd ComponentからAR Plane Manager と AR Raycast Managerを追加
Cubeと空のGameobjectを作成し、名前をTimelineに変更
project上で右クリック→Create→C# Script で名前を「ARtimeline」に変更
ARtimelineスクリプトを下記に書き換え。
ARtimeline.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.XR.ARFoundation; using UnityEngine.XR.ARSubsystems; using UnityEngine.Playables; namespace ARtimeline{ public class ARtimeline : MonoBehaviour { public GameObject timelineobj; private ARRaycastManager raycastManager; private static List<ARRaycastHit> hits = new List<ARRaycastHit>(); private bool active = false; public GameObject playablesobj; private PlayableDirector director; // Start is called before the first frame update void Start() { raycastManager = GetComponent<ARRaycastManager>(); timelineobj.SetActive(active); director = playablesobj.GetComponent<PlayableDirector>(); } // Update is called once per frame void Update() { if(Input.touchCount > 0) { Vector2 touchPosition = Input.GetTouch(0).position; if(raycastManager.Raycast(touchPosition, hits, TrackableType.Planes)) { var hitPose = hits[0].pose; if(active) { timelineobj.transform.position = hitPose.position; director.Play(); } else { active = true; timelineobj.SetActive(active); } } } } } }何をしているかというと、簡単には、
画面をタッチするとCubeが表示され、その位置が画面でタッチしたAR上で検出した平面ポジションに配置
その後timelineを動かす
ってな感じ。引用
https://qiita.com/shun-shun123/items/1aa646049474d0e244be
スクリプトとGameobjectのアタッチお忘れなく!
2.Timelineを作る
Timelineを選択し、window→Sequencing→Timeline でTimelineの画面を出し
Createボタンを押します。
すると、Playable Directorというコンポーネントが追加されます。
このPlay On Awakeのチェックを外します。
3.演出のコードを書く
Timelineの演出をコードでつくるわけだが、そのスクリプトはオブジェクトにアタッチする必要がなくproject内にあれば良い。
その必要なスクリプトは3つ。
・1つめが「ControllTrack」
主にTimelineのトラックを設定する。
project内にC# Scriptを作って名前を「ControllTrack」にします。
下記コードに変更ControllTrack.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; namespace ARtimeline{ [TrackClipType(typeof(ControllAsset))] [TrackBindingType(typeof(GameObject))] public class ControllTrack : TrackAsset{} }ちなみに
[TrackBindingType(typeof(GameObject))]はGameObjectだけでなく
[TrackBindingType(typeof(コンポーネント名))]でもOK
例えば、Light入れたり、Transform入れたりなんでも・2つめが「ControllAsset」
これはTimeline内で動きを格納する箱みたいなもん
コードはこちらControllAssetusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; namespace ARtimeline{ public class ControllAsset : PlayableAsset { public override Playable CreatePlayable(PlayableGraph graph, GameObject owner) { var playable = ScriptPlayable<ControllBehaviour>.Create(graph); var ControllBehaviour = playable.GetBehaviour(); return playable; } } }・3つめが「ControllBehaviour」
これが実際に動きをつけたり、いろいろいじれるスクリプト
同じように、C# Script作って、コードは下記ControllBehaviourusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; namespace ARtimeline{ public class ControllBehaviour : PlayableBehaviour { private GameObject timelineobj; public override void ProcessFrame(Playable playable, FrameData info, object playerData) { timelineobj = playerData as GameObject; timelineobj.transform.position += timelineobj.transform.forward * 0.1f * Time.deltaTime; } } }これは
timelineobj.transform.position += timelineobj.transform.forward * 0.1f * Time.deltaTime;ここで、GameObject(ここで言うならCube)を動かしています
PlayableAPIについて詳しく知りたい方は下記から。
このチュートリアルが一番いい
https://blogs.unity3d.com/jp/2018/09/05/extending-timeline-a-practical-guide/
4.コードをTimelineに実装する
右クリック→ARtimeline→Controll Trackでトラックを作ります
トラックの右の●3つからAdd Controll Assetを選択
Timelineを再生すると、Cubeが動くのが確認できます。
5.実機ビルド
File→Build settingから
Add Open Scenesボタンを押して、sceneを追加
platformをiosに変更して、switch platform
Player Settingsを開き、OtherSettingsの中の、Camera Usage Discription(カメラ使用しますぞのメッセージ)を入力します。
同じくOther Settingsの中の、ArchitectureをARM64にします。
同じくOther Settingsの中のTarget minimum iOS Versionを11.0にします。これでbuildしましょう。
すると、xcodeのファイルが作られるので、これを開き、signingのteamを設定し実機ビルドしましょう
https://uni.gas.mixh.jp/unity/unity-for-ios.html完成
ここに音や他のTrackを入れれば、いい感じになるのでは、、、
あとは開発者次第ですねTimelineは奥が深い
Timelineを使えばお手軽に演出を作れるが、ARのように少し込み入ったことをしようとすると、ガッツリ拡張しないといけません。
そんな迷路に入りたい方はこちら
https://www.youtube.com/watch?v=6SPpjSKy9LI
- 投稿日:2020-09-28T18:55:44+09:00
低レベルネイティブプラグインインターフェースのメモ
元ネタ
https://github.com/Unity-Technologies/NativeRenderingPlugin
https://qiita.com/fukaken5050/items/63940b65ec79a59161eb環境
OS : win10pro
IDE : AndroidStudio 4.0.1
Unity : 2019.4.5f1
Unity/VS : VS2019
確認した実機: Galaxys8要点の箇条書き
低レベルネイティブプラグインインターフェース(Low-Level Native Plugin Interface)を使ってUnityAndroid/Pluginで動画を再生する。
・AndroidStudio/C++で次を#includeする。Unityインストールしたフォルダからコピペする。
#include "IUnityInterface.h" #include "IUnityGraphics.h" // UnityRenderingEvent・Unityプラグインの更新はUnity再起動が必要。
→ ホットリロード検索、名前を変えて追加するなど・今回はOpenGLESを使う。多分Vulkanでもできると思う。
基本知識
◆ 低レベルネイティブプラグインインターフェース
Unityのレンダリングは、マルチスレッドです。レンダリング APIは、MonoBehaviourスレッドとは別のスレッド上で行われます。プラグインはUnityレンダースレッドと干渉する可能性があります。そのため、GL.IssuePluginEvent でメインスレッドからプラグインを呼びます。
Unityカメラの MonoBehaviour.OnPostRender から GL.IssuePluginEvent を呼び出す場合、プラグインはカメラのレンダリング終了後すぐにコールバックを取得します。UnityRenderingEventを使うには IUnityGraphics.h をincludeします。
◆OpenGL グラフィックス API を使用したプラグイン
2種類の OpenGL オブジェクトがあります。
1.OpenGL コンテキストをまたいで共有されるオブジェクト (texture, renderBuffer,shader等)
2.OpenGL コンテキストごとのオブジェクト(頂点配列,program pipeline等)Unity は複数の OpenGL コンテキストを使用します。
エディターとプレイヤーの初期化と終了のときは
マスターコンテキストに依存しますが、レンダリングには専門のコンテキストを使用します。
したがって、kUnityGfxDeviceEventInitialize と kUnityGfxDeviceEventShutdown イベントの間、
コンテキストごとのオブジェクトを作成することはできません。例えば、ネイティブのプラグインは kUnityGfxDeviceEventInitialize イベント中に頂点配列オブジェクトを作成できず、
UnityRenderingEvent コールバックでそれを使用することはできません。
これは、アクティブなコンテキストが頂点配列オブジェクトの作成時に使用されるものでないからです。◆ Unity c# からnative plugin(C++)を呼ぶ方法(1)
[DllImport("nativerender")] private static extern bool SetupNativeTextureRender(IntPtr textureId, int width, int height);Javaのように"com.example.hoge"は意識しなくても良いです。
プラグイン配置場所はUnityApp\Assets\Plugins\Androidです(Androidの場合)
プラグインの名前が重要です。”libnativerender.so”の場合はlib/.soを除去して[DllImport("nativerender")]です。
プラグインの更新後はUnity再起動が必要です。
void*の代わりにIntPtrを使います。メモ
◆初期化
Unityのprivate void Start() でプラグインの初期化をします。
UnityのTexture2Dを生成して、テクスチャアドレスtexture.GetNativeTexturePtr()をプラグインに渡します。
_rawImage.textureをUnity-RawImage等にセットして描画結果を確認します。var texture = new Texture2D(_width, _height, TextureFormat.ARGB32, false); _rawImage.texture = texture; SetupNativeTextureRender(texture.GetNativeTexturePtr(), texture.width,texture.height) ; StartCoroutine(NativeTextureRenderLoop()); //コルーチン起床◆コルーチン/描画
private IEnumerator NativeTextureRenderLoop() { while (true) { yield return new WaitForEndOfFrame(); GL.IssuePluginEvent(GetRenderEventFunc(), 1); } }コード C++ (拝借してます)
#include "IUnityInterface.h" #include "IUnityGraphics.h" // UnityRenderingEvent #include <math.h> #include <stdio.h> #include <string> #include <assert.h> #include <GLES2/gl2.h> #include <jni.h> static GLuint g_textureId = NULL; static int g_texWidth; static int g_texHeight; static u_char* g_pBytes = NULL; #define LOG_PRINTF printf #if 1 extern "C" bool SetupNativeTextureRender( void* textureId, int width, int height ) { g_textureId = (GLuint)(size_t)textureId; g_texWidth = width; g_texHeight = height; LOG_PRINTF( "SetupNativeTextureRender:%d, %d, %d", g_textureId, g_texWidth, g_texHeight ); g_pBytes = new u_char[ g_texWidth * g_texHeight * 4 ]; return true; } extern "C" void FinishNativeTextureRender() { if( g_pBytes != NULL ) delete[] g_pBytes; g_pBytes = NULL; } static void UNITY_INTERFACE_API OnRenderEvent( int eventID ) { glBindTexture( GL_TEXTURE_2D, g_textureId ); static u_char s_r = 0; u_char* bytes = g_pBytes; for( int y = 0; y < g_texHeight; y++ ) { for( int x = 0; x < g_texWidth; x++ ) { int offset = ( ( y * g_texWidth ) + x ) * 4; bytes[ offset + 0 ] = s_r; bytes[ offset + 1 ] = 0; bytes[ offset + 2 ] = 0; bytes[ offset + 3 ] = 255; } } glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, g_texWidth, g_texHeight, GL_RGBA, GL_UNSIGNED_BYTE, bytes ); s_r ++; s_r %= 255; } extern "C" UnityRenderingEvent UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API GetRenderEventFunc() { return OnRenderEvent; } //com.ore.unityplugin extern "C" JNIEXPORT jstring JNICALL Java_com_ore_unityplugin_Calc_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "hoge from C++"; return env->NewStringUTF(hello.c_str()); } #endifUnity
#define USE_ANDROID_PLUGIN using System; using System.Collections; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UI; public class TestNativeCppRender : MonoBehaviour { [SerializeField] private RawImage _rawImage = null; [SerializeField] private int _width = 512; [SerializeField] private int _height = 512; public Text m_text; //PluginFunction //nativerender [DllImport("nativerender")] private static extern bool SetupNativeTextureRender(IntPtr textureId, int width, int height); [DllImport("nativerender")] private static extern void FinishNativeTextureRender(); [DllImport("nativerender")] private static extern IntPtr GetRenderEventFunc(); private void Start() { var texture = new Texture2D(_width, _height, TextureFormat.ARGB32, false); _rawImage.texture = texture; #if USE_ANDROID_PLUGIN if (SetupNativeTextureRender(texture.GetNativeTexturePtr(), texture.width, texture.height) == false) { m_text.text = "fail SetupNativeTextureRender"; return; } StartCoroutine(NativeTextureRenderLoop()); m_text.text = "after StartCoroutine"; #else m_text.text = " editor"; StartCoroutine(NativeTextureRenderLoop()); #endif } private void OnDestroy() { FinishNativeTextureRender(); } private IEnumerator NativeTextureRenderLoop() { int cnt = 0; while (true) { yield return new WaitForEndOfFrame(); GL.IssuePluginEvent(GetRenderEventFunc(), 1); m_text.text = String.Format("cnt = {0}", cnt); cnt++; } } }
- 投稿日:2020-09-28T18:55:44+09:00
低レベルネイティブプラグインインターフェースのメモ1
元ネタ
https://github.com/Unity-Technologies/NativeRenderingPlugin
https://qiita.com/fukaken5050/items/63940b65ec79a59161eb環境
OS : win10pro
IDE : AndroidStudio 4.0.1
Unity : 2019.4.5f1
Unity/VS : VS2019
確認した実機: Galaxys8要点の箇条書き
低レベルネイティブプラグインインターフェース(Low-Level Native Plugin Interface)を使ってUnityAndroid/Pluginで動画を再生する。
・AndroidStudio/C++で次を#includeする。Unityインストールしたフォルダからコピペする。
#include "IUnityInterface.h" #include "IUnityGraphics.h" // UnityRenderingEvent・Unityプラグインの更新はUnity再起動が必要。
→ ホットリロード検索、名前を変えて追加するなど・今回はOpenGLESを使う。多分Vulkanでもできると思う。
基本知識
◆ 低レベルネイティブプラグインインターフェース
Unityのレンダリングは、マルチスレッドです。レンダリング APIは、MonoBehaviourスレッドとは別のスレッド上で行われます。プラグインはUnityレンダースレッドと干渉する可能性があります。そのため、GL.IssuePluginEvent でメインスレッドからプラグインを呼びます。
Unityカメラの MonoBehaviour.OnPostRender から GL.IssuePluginEvent を呼び出す場合、プラグインはカメラのレンダリング終了後すぐにコールバックを取得します。UnityRenderingEventを使うには IUnityGraphics.h をincludeします。
◆OpenGL グラフィックス API を使用したプラグイン
2種類の OpenGL オブジェクトがあります。
1.OpenGL コンテキストをまたいで共有されるオブジェクト (texture, renderBuffer,shader等)
2.OpenGL コンテキストごとのオブジェクト(頂点配列,program pipeline等)Unity は複数の OpenGL コンテキストを使用します。
エディターとプレイヤーの初期化と終了のときは
マスターコンテキストに依存しますが、レンダリングには専門のコンテキストを使用します。
したがって、kUnityGfxDeviceEventInitialize と kUnityGfxDeviceEventShutdown イベントの間、
コンテキストごとのオブジェクトを作成することはできません。例えば、ネイティブのプラグインは kUnityGfxDeviceEventInitialize イベント中に頂点配列オブジェクトを作成できず、
UnityRenderingEvent コールバックでそれを使用することはできません。
これは、アクティブなコンテキストが頂点配列オブジェクトの作成時に使用されるものでないからです。◆ Unity c# からnative plugin(C++)を呼ぶ方法(1)
using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using UnityEngine; [DllImport("nativerender")] private static extern bool SetupNativeTextureRender(IntPtr textureId, int width, int height);Javaのように"com.example.hoge"は意識しなくても良いです。
プラグイン配置場所はUnityApp\Assets\Plugins\Androidです(Androidの場合)
プラグインの名前が重要です。”libnativerender.so”の場合はlib/.soを除去して[DllImport("nativerender")]です。
プラグインの更新後はUnity再起動が必要です。
void*の代わりにIntPtrを使います。メモ
◆初期化
Unityのprivate void Start() でプラグインの初期化をします。
UnityのTexture2Dを生成して、テクスチャアドレスtexture.GetNativeTexturePtr()をプラグインに渡します。
_rawImage.textureをUnity-RawImage等にセットして描画結果を確認します。var texture = new Texture2D(_width, _height, TextureFormat.ARGB32, false); _rawImage.texture = texture; SetupNativeTextureRender(texture.GetNativeTexturePtr(), texture.width,texture.height) ; StartCoroutine(NativeTextureRenderLoop()); //コルーチン起床◆コルーチン/描画
private IEnumerator NativeTextureRenderLoop() { while (true) { yield return new WaitForEndOfFrame(); GL.IssuePluginEvent(GetRenderEventFunc(), 1); } }メモ
SetupNativeTextureRender( unityで生成したテクスチャメモリ,w,h)をC++へ渡す。->g_pBytes,g_textureId
g_pBytesを更新する。
UnityRenderingEvent():
glBindTexture( GL_TEXTURE_2D, g_textureId );でg_textureId を有効にする。
glTexSubImage2D( GL_TEXTURE_2D...) にg_pBytesを渡してOpenGLESテクスチャを更新するコード C++ (拝借してます)
#include "IUnityInterface.h" #include "IUnityGraphics.h" // UnityRenderingEvent #include <math.h> #include <stdio.h> #include <string> #include <assert.h> #include <GLES2/gl2.h> #include <jni.h> static GLuint g_textureId = NULL; static int g_texWidth; static int g_texHeight; static u_char* g_pBytes = NULL; #define LOG_PRINTF printf #if 1 extern "C" bool SetupNativeTextureRender( void* textureId, int width, int height ) { g_textureId = (GLuint)(size_t)textureId; g_texWidth = width; g_texHeight = height; LOG_PRINTF( "SetupNativeTextureRender:%d, %d, %d", g_textureId, g_texWidth, g_texHeight ); g_pBytes = new u_char[ g_texWidth * g_texHeight * 4 ]; return true; } extern "C" void FinishNativeTextureRender() { if( g_pBytes != NULL ) delete[] g_pBytes; g_pBytes = NULL; } static void UNITY_INTERFACE_API OnRenderEvent( int eventID ) { glBindTexture( GL_TEXTURE_2D, g_textureId ); static u_char s_r = 0; u_char* bytes = g_pBytes; for( int y = 0; y < g_texHeight; y++ ) { for( int x = 0; x < g_texWidth; x++ ) { int offset = ( ( y * g_texWidth ) + x ) * 4; bytes[ offset + 0 ] = s_r; bytes[ offset + 1 ] = 0; bytes[ offset + 2 ] = 0; bytes[ offset + 3 ] = 255; } } glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, g_texWidth, g_texHeight, GL_RGBA, GL_UNSIGNED_BYTE, bytes ); s_r ++; s_r %= 255; } extern "C" UnityRenderingEvent UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API GetRenderEventFunc() { return OnRenderEvent; } //com.ore.unityplugin extern "C" JNIEXPORT jstring JNICALL Java_com_ore_unityplugin_Calc_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "hoge from C++"; return env->NewStringUTF(hello.c_str()); } #endifUnity
#define USE_ANDROID_PLUGIN using System; using System.Collections; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UI; public class TestNativeCppRender : MonoBehaviour { [SerializeField] private RawImage _rawImage = null; [SerializeField] private int _width = 512; [SerializeField] private int _height = 512; public Text m_text; //PluginFunction //nativerender [DllImport("nativerender")] private static extern bool SetupNativeTextureRender(IntPtr textureId, int width, int height); [DllImport("nativerender")] private static extern void FinishNativeTextureRender(); [DllImport("nativerender")] private static extern IntPtr GetRenderEventFunc(); private void Start() { var texture = new Texture2D(_width, _height, TextureFormat.ARGB32, false); _rawImage.texture = texture; #if USE_ANDROID_PLUGIN if (SetupNativeTextureRender(texture.GetNativeTexturePtr(), texture.width, texture.height) == false) { m_text.text = "fail SetupNativeTextureRender"; return; } StartCoroutine(NativeTextureRenderLoop()); m_text.text = "after StartCoroutine"; #else m_text.text = " editor"; StartCoroutine(NativeTextureRenderLoop()); #endif } private void OnDestroy() { FinishNativeTextureRender(); } private IEnumerator NativeTextureRenderLoop() { int cnt = 0; while (true) { yield return new WaitForEndOfFrame(); GL.IssuePluginEvent(GetRenderEventFunc(), 1); m_text.text = String.Format("cnt = {0}", cnt); cnt++; } } }
- 投稿日:2020-09-28T16:33:18+09:00
【Unity】Texture2Dを毎フレーム更新する処理を書いたらメモリリークした
概要
ある GameObject の Texture を毎フレーム更新して動画のように描画するコードを書いていたところ、ネイティブ環境で異常にクラッシュする事象が発生した。その時に調査した手法と原因をまとめておく。
調査手法
下記のようなエラーを事前に吐いていたので、メモリ関連であるということは分かっていた。 (WebGL)
Uncaught RangeError : Maximum Call Stack Size Exceeded次に Profiler からメモリ使用量を確認すると、Unity メモリが 3GB を超えていることを確認。(上昇ペースも1秒に数十MBだった)
更に、メモリビューの Detailed から何がメモリを食っているのか確認すると、画像のように Texture2D が毎フレームごとに 2.3MB メモリ確保していることが判明した。
Texture2D をこんなに生成している箇所は 1 箇所しか心当たりが無かったので、該当コードを確認することにした。
原因
該当コードは下記の通り。
// frameはtexture情報をバイナリで保持している独自クラス var texture2D = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false); texture2D.wrapMode = TextureWrapMode.Clamp; texture2D.LoadRawTextureData(frame.Buffer); texture2D.Apply(); material.mainTexture = texture2D;ここで問題になっているのは
new Texture2D()
の部分で、今回確保されたメモリは今フレームではmaterial.mainTexture
が参照するが、material.mainTexture
は次フレームでは別の参照を保持している。そのため、前フレームに確保したTexture2D
のメモリ領域はどこからも参照されなくなり、Unity メモリなので GC も行われずそのままリークするといった事が原因であった。解決策
前フレームで使用していた
Texture2D
のメモリを手動で解放する事で解決。
Unity で利用するアセットのメモリ解放はMonoBehaviour.Destroy()
で出来る。
実際の修正済みコードは下記の通り。var texture2D = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false); texture2D.wrapMode = TextureWrapMode.Clamp; texture2D.LoadRawTextureData(frame.Buffer); texture2D.Apply(); // これを追加した Destroy(material.mainTexture); material.mainTexture = texture2D;これで無事にメモリリークは解消され、ネイティブ環境でアプリがクラッシュすることも無くなった。
まとめ
「アプリが重い、メモリ関係でクラッシュする」といった場合はまず「Profiler」を確認すること。
闇雲にあたりを付けて修正してもコストが掛かるだけで改善するとは限らない。Profiler を使った最適化は 「【Unite 2017 Tokyo】最適化をする前に覚えておきたい技術」の動画を見たメモ にまとめてあるので、全く縁が無かった方は読んでおくと良いと思われる。
※ 調査方法や解決方法で更に効率的な方法などがあればコメントを頂けますと幸いです。
- 投稿日:2020-09-28T15:58:03+09:00
OculusQuestのハンドトラッキングで右手左手を簡単に判別する
1.はじめに
技術研究として2019年12月に実装された、OculusQuestのハンドトラッキング技術を用いた開発を行いました。
途中オブジェクトを掴むとき右手と左手の判別をしたくて簡単に実装してみました。
ちなみに開発したものはこちら ↓
・https://lab.taki.co.jp/virtual-cube-puzzle/2.問題点
OculusQuestのハンドトラッキングでオブジェクトを掴むときの流れとして
手がオブジェクトに触れる → オブジェクト側が触れたことを検知 → 触れた状態でピンチ → 手のオブジェクトの子オブジェクトになる → ピンチ解除 → 親子関係解除
という感じです。
物体を掴むことに関してはこちらの記事を参考にさせていただきました。
https://qiita.com/Kujirai_sakananosuke/items/4ea801e0ed3e08e1cccb最初の手がオブジェクトに触れるという部分は
OnCollisionStay()
で当たり判定を付けます。
UnityのAssetStoreで提供されているOculus IntegrationのOVRHandPrefabからだと関節ごとのBone情報で手の当たり判定を検知できるのですが、左右の手に入っているBoneの名前が完全に一緒のため手が触れたことまでで、左右どちらの手が触れたかの判別ができません。
今回は左右判別する部分だけ抽出してまとめます。3.実装
調べると色々やり方はありそうでしたが時間がなかったので簡単な方法で実装しました。
左右のOVRHandPrefabと同階層に3Dオブジェクトをそれぞれ入れて別々に当たり判定を付けることにしました。
OculusQuestのハンドトラッキングで右手左手判別する簡単な方法 pic.twitter.com/m6tfMZ8tJH
— Usuba Masato (@usbhatyu) September 28, 2020上記のようにRightCube,LeftCubeを追加したらそれぞれスクリプトをアタッチします。
RightCube.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class RightCube : MonoBehaviour { public bool _TouchingRight; void OnCollisionStay(Collision Collision) { _TouchingRight = true; } void OnCollisionExit(Collision Collision) { _TouchingRight = false; } }LeftCube.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class LeftCube : MonoBehaviour { public bool _TouchingLeft; void OnCollisionStay(Collision Collision) { _TouchingLeft = true; } void OnCollisionExit(Collision Collision) { _TouchingLeft = false; } }RightCube,LeftCubeは簡単でこれだけです。
続けて触れられる側のオブジェクトにもスクリプトをアタッチします。(上の画像のHierarchy内でいうと一番下のCube)TouchCube.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class TouchCube : MonoBehaviour { private RightCube _RightCube; private LeftCube _LeftCube; private bool _touchRight; private bool _touchLeft; void Start() { _RightCube = FindObjectOfType<RightCube>(); _LeftCube = FindObjectOfType<LeftCube>(); } void Update() { _touchRight = _RightCube._TouchingRight; _touchLeft = _LeftCube._TouchingLeft; if(_touchRight == false && _touchLeft == false){ GetComponent<Renderer>().material.color = Color.gray; } } void OnCollisionStay(Collision other) { if (other.gameObject.name == "Hand_Index2_CapsuleRigidBody" || other.gameObject.name == "Hand_Index1_CapsuleRigidBody" || other.gameObject.name == "Hand_Thumb2_CapsuleRigidBody" || other.gameObject.name == "Hand_Thumb1_CapsuleRigidBody" ) { if(_touchRight == true && _touchLeft == false){ GetComponent<Renderer>().material.color = Color.magenta; }else if(_touchLeft == true && _touchRight == false){ GetComponent<Renderer>().material.color = Color.cyan; }else if(_touchRight == true && _touchLeft == true){ GetComponent<Renderer>().material.color = Color.yellow; } } } }左右の判別ができると別々の処理ができる pic.twitter.com/fl0Ooedw6q
— Usuba Masato (@usbhatyu) September 28, 2020今回は触れる物体が一つなので問題ありませんが、左右の判別だけだとオブジェクトが複数ある時に一つ触れると他のオブジェクトも連動してスクリプトを走らせてしまいます。
なのでBoneを検知したうえで右手か左手かを判別させています。最後にRightCube,LeftCubeにAlpha値0にしたマテリアルをアタッチすればオッケーです。
以上スマートではないけど簡単な右手左手の判別方法でした。