20200928のUnityに関する記事は8件です。

VRCSDK3 でバーチャル早着替え with VRoid

みなさん、VRChat 楽しんでいますか? 私はとても楽しんでいます。

さて、先日 VRCSDK3-AVATAR がリリースされました。その特徴は、なんといってもカスタマイズ性の高さです。もとよりプレイヤー自身がアバターやワールドを制作することが VRChat の醍醐味ではありますが、 その長所がより突き詰められた形です。

本記事では、従前のマテリアルアニメーションとアクションメニューのカスタマイズを組み合わせることで、アバターを切り替えることなく衣装をチェンジするバーチャル早着替えを実装します。

STEP0: VRM 素材の準備

VRoid で普通にアバターを作成します。

vroid-henshin-2.jpg

次に、着替える衣装を用意します。着替える前の服と後の服はモデルの切り替えではなく、テスクチャのレイヤー切り替えで表現してください。今回はマテリアル(=テクスチャ+シェーダ)を差し替えることで早着替えを行うためです。1

vroid-henshin-1.jpg

衣装が定まったら、着替え前・後で2つの VRM ファイルを出力してください。VRoid での作業は終了です。

STEP1: Unity プロジェクトへのインポート

Unity プロジェクトに VRCSDK3-AVATARVRM Converter for VRChat をインポートします。

vroid-henshin-3.jpg

2つの VRM ファイルをインポートします。続いて、VRM Converter 所定の手順に従い、着替え前のモデルだけ VRChat 用に変換します。

vroid-henshin-4.jpg

シーン中にある VRChat 用モデルを複製します。2

vroid-henshin-5.jpg

STEP3: アニメーションの作成

Animationウィンドウ(タブ)で [Create] をクリックし、新しいアニメーションクリップを作成します。名前は何でもよいですが、ここでは kigae としました。

vroid-henshin-6.jpg

Animationウィンドウ(タブ)の左上にある記録ボタン(赤丸)を押し、アニメーションの記録を開始します。

vroid-henshin-7.jpg

足元に複製したモデルが埋まっているので、ヒエラルキーから [Body] を選択しインスペクタを開きます。[Skined Mesh Renderer]-[Materials]で任意のマテリアルを着替え後のマテリアルに差し替えます。ワンピースの場合はトップスのみなので、F00_002_01_Tops_01_CLOTH を変更すれば OK です。

vroid-henshin-8.jpg

変更ができたら、再度記録ボタンを押してアニメーションの作成を終了します。複製したモデルは削除しておきましょう。

STEP4: FX の変更

FX はモデルの状態遷移図のようなものです。VRM Converter で変換したモデルでは、あらかじめカスタマイズされた FX が作成されています。

モデルのインスペクタで [VRC Avatar Descriptor]-[Playable Layers]-[FX] に設定されている FX をダブルクリックで開きましょう。

vroid-henshin-9.jpg

[Parameters] に int 型変数 Kigae を追加します。この変数はアクションメニューとの連携に利用します。アクションメニューでの操作が変数に反映されるので、FX では Kigae の変化で服装が変化するようにしましょう。

vroid-henshin-10.jpg

FX は複数の状態遷移図をレイヤーで重ねたような構造になっています。[Layers] タブで Kigae レイヤーを追加し、オプションで Weight を 1 に変更します。デフォルトでは Weight が 0 ですが、そのままではモデルにアニメーションが反映されません。

vroid-henshin-11.jpg

状態遷移を作図していきます。右クリックで [Create State]-[Empty] から新たな状態を、状態を右クリックし [Make Transition] から新たな遷移を作成できます。下図のように「着替え前の状態」と「着替え後の状態」を作り、「Entry」から「Exit」がつながるようにしてください。

そして、「着替え後の状態」の [Motion] に先ほど作成したアニメーションを設定します。

vroid-henshin-12.jpg

Transition を選択し、遷移の条件を設定します。[Conditions] で Kigae equals 0 で「着替え前の状態」に、Kigae equals 1 で「着替え後の状態」へ遷移するようにします。

vroid-henshin-13.jpg

vroid-henshin-14.jpg

これで FX の設定は完了です。

STEP4: アクションメニュー の変更

最後に、設定したアニメーションをアクションメニューから呼び出せるようにしましょう。インスペクタで [VRC Avatar Descriptor]-[Expressions]-[VRCExpressionParameters] を開きます。

vroid-henshin-15.jpg

ここで、先ほど設定した変数 Kigae との紐付けを行います。変数名と型を設定します。

vroid-henshin-16.jpg

[VRC Avatar Descriptor]-[Expressions]-[VRCExpressionsMenu] を開きます。

vroid-henshin-17.jpg

[Add Components] からアクションメニューの項目を追加します。

[Parameter]で Kigae, Int を選択し、[Value] を 1 にします。

[Type] で toggle を指定すると、次にボタンを押すまで着替えっぱなしになります。

vroid-henshin-18.jpg

ビルド&テストしてみましょう。

vroid-henshin-19.jpg

vroid-henshin-20.jpg

アクションメニューに項目が追加され、早着替えを行うことができました!


  1. Blender等を利用しVRoid製モデルを素体+衣装で分割する場合は、マテリアル切り替えではなくオブジェクトの表示・非表示を利用した着替えが可能ですが、本記事の範囲外とします。 

  2. この後アニメーションを作成する際に地面に埋まってしまうと直すのが面倒なので、複製しておくことがおすすめです。 

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

【Unity】ボタンが押せないのは大体RaycastTargetの設定ミス

UIが反応しない…

スクリーンショット 2020-09-28 21.52.46.png
上の画像のようにボタンのUIをしっかり設定しているのに、なぜか押せない時ってありますよね?
そんな時は大体RaycastTargetの設定ミスです。

RaycastTargetとは

まず、Raycastというのは指定した場所からRay(光線)を放ち、光線と接触したオブジェクトの情報を取得する機能になります。
銃を撃って敵を攻撃する時に使用したり、本記事のテーマであるUIの情報を取得したりと様々な場面で使うことのできる機能です。

ではRaycastTargetとは何か?
もうお分かりだと思いますが、その名の通りRaycastTarget(対象)とするか否かを設定するパラメータになります。
これをtrueにするとRaycastTarget(対象)となるので、情報を取得することができるということです。

ボタンが押せない理由

本記事最初の画像のボタンが押せなかった理由は、以下の二つが同時に発生している時になります。

  • 「Helloテキスト」がボタンより手前にある
  • 「Helloテキスト」のRaycastTargettrue

手前かつRaycastTargettrueになっていると、背後のオブジェクトまでRayが通らないのでボタンが反応しない現象が起きるのです。
テキストボックスは見かけを整えるために、実際に見えてる範囲以上の大きさを持っている場合があり、ボタンと重なってしまうことが多いので要注意です。
スクリーンショット 2020-09-28 22.17.29.png
見かけよりでかいテキスト

image.png
trueになってしまっているRaycastTarget

解決策

基本的にボタン以外はRaycastTargetfalseにすることで解決します。
新しくUIを作成する時は注意してみてください。
それでも反応しない場合はボタンのOnClickが設定されていないか、シーン内にEventSystemがないかだと思われます。

まとめ

  • RaycastRay(光線)を放ち、光線と接触したオブジェクトの情報を取得する機能
  • 基本的にボタン以外はRaycastTargetfalseにしよう
  • それでも反応しない場合はOnClickを設定しているか確認しよう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 で言及されていたので、補足情報として引用させていただきます。ありがとうございます。

公式サンプル

公式ドキュメント上に MRTKを使った Unity 向けのサンプル ( GitHub ) が公開されていました。

GitHub ページ : QRTracking/SampleQRCodes

今回はこちらのサンプルを動作させる手順についてご紹介したいと思います。

概要 / Overview

QRコードを検出した際、QRコードの上に白い四角のCGが表示されます。
その他にQRコードから読み取った関連情報も表示されます。

  • GUID ( Globally Unique Identifier : グローバル識別子 )
  • Physical size
  • Timestamp
  • decode data

検証環境

  • 開発用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.git
2. 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

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

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ですが
こんなやつ


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:12

1.ARの開発準備をする

projectを作成したら
まず、main cameraを削除し、
window → package managerを開き、ARFoundationをinstallします。
もしARFoundationが出てこない場合は、Packages:をUnity Registryにしてみてください。

ヒエラルキー上で右クリック → XR → AR Session Origin と AR Session を追加
スクリーンショット 2020-09-23 20.01.22.png

AR Session OriginにAdd ComponentからAR Plane Manager と AR Raycast Managerを追加
スクリーンショット 2020-09-24 17.04.58.png

Cubeと空のGameobjectを作成し、名前をTimelineに変更
project上で右クリック→Create→C# Script で名前を「ARtimeline」に変更
スクリーンショット 2020-09-24 17.04.58.png

ARtimelineスクリプトを下記に書き換え。

ARtimeline.cs
using 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のアタッチお忘れなく!
スクリーンショット 2020-09-28 19.48.01.png

2.Timelineを作る

Timelineを選択し、window→Sequencing→Timeline でTimelineの画面を出し
Createボタンを押します。
スクリーンショット 2020-09-24 17.53.22.png

すると、Playable Directorというコンポーネントが追加されます。
このPlay On Awakeのチェックを外します。
スクリーンショット 2020-09-28 16.44.07.png

3.演出のコードを書く

Timelineの演出をコードでつくるわけだが、そのスクリプトはオブジェクトにアタッチする必要がなくproject内にあれば良い。

その必要なスクリプトは3つ。

・1つめが「ControllTrack」
主にTimelineのトラックを設定する。
project内にC# Scriptを作って名前を「ControllTrack」にします。
下記コードに変更

ControllTrack.cs
using 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内で動きを格納する箱みたいなもん
コードはこちら

ControllAsset
using 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作って、コードは下記

ControllBehaviour
using 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://qiita.com/hadashiA/items/566f0d0222cb9a4e209b

このチュートリアルが一番いい

https://blogs.unity3d.com/jp/2018/09/05/extending-timeline-a-practical-guide/

4.コードをTimelineに実装する

右クリック→ARtimeline→Controll Trackでトラックを作ります
スクリーンショット 2020-09-28 12.31.54.png

トラックの右の●3つからAdd Controll Assetを選択
スクリーンショット 2020-09-28 12.33.27.png

最後にCubeをトラックにアタッチします。
cubeattach.gif

Timelineを再生すると、Cubeが動くのが確認できます。
cubetrans.gif

5.実機ビルド

File→Build settingから
Add Open Scenesボタンを押して、sceneを追加
スクリーンショット 2020-09-28 17.15.12.png

platformをiosに変更して、switch platform
スクリーンショット 2020-09-28 17.16.05.png

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

完成

arplane.gif
ここに音や他のTrackを入れれば、いい感じになるのでは、、、
あとは開発者次第ですね

Timelineは奥が深い

Timelineを使えばお手軽に演出を作れるが、ARのように少し込み入ったことをしようとすると、ガッツリ拡張しないといけません。
そんな迷路に入りたい方はこちら
https://www.youtube.com/watch?v=6SPpjSKy9LI

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

低レベルネイティブプラグインインターフェースのメモ

元ネタ

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());
}

#endif


Unity

#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++;
        }
    }
}

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

低レベルネイティブプラグインインターフェースのメモ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());
}

#endif


Unity

#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++;
        }
    }
}

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

【Unity】Texture2Dを毎フレーム更新する処理を書いたらメモリリークした

概要

ある GameObject の Texture を毎フレーム更新して動画のように描画するコードを書いていたところ、ネイティブ環境で異常にクラッシュする事象が発生した。その時に調査した手法と原因をまとめておく。

調査手法

下記のようなエラーを事前に吐いていたので、メモリ関連であるということは分かっていた。 (WebGL)

Uncaught RangeError : Maximum Call Stack Size Exceeded

次に Profiler からメモリ使用量を確認すると、Unity メモリが 3GB を超えていることを確認。(上昇ペースも1秒に数十MBだった)

image.png

更に、メモリビューの Detailed から何がメモリを食っているのか確認すると、画像のように Texture2D が毎フレームごとに 2.3MB メモリ確保していることが判明した。

image.png

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】最適化をする前に覚えておきたい技術」の動画を見たメモ にまとめてあるので、全く縁が無かった方は読んでおくと良いと思われる。

※ 調査方法や解決方法で更に効率的な方法などがあればコメントを頂けますと幸いです。

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

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オブジェクトをそれぞれ入れて別々に当たり判定を付けることにしました。
Hierarchy.png

上記のようにRightCube,LeftCubeを追加したらそれぞれスクリプトをアタッチします。

RightCube.cs
using 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.cs
using 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.cs
using 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;
            }
        }
    }
}

今回は触れる物体が一つなので問題ありませんが、左右の判別だけだとオブジェクトが複数ある時に一つ触れると他のオブジェクトも連動してスクリプトを走らせてしまいます。
なのでBoneを検知したうえで右手か左手かを判別させています。

最後にRightCube,LeftCubeにAlpha値0にしたマテリアルをアタッチすればオッケーです。
以上スマートではないけど簡単な右手左手の判別方法でした。

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