e.blog

主にUnity/UE周りのことについてまとめていきます

UnrealEngineでVRことはじめ(C++編)

概要

Unreal Engineで、VR向けコンテンツをC++を使って作るための「ことはじめ」を書いていきたいと思います。
大体の書籍を見ていると、ブループリントの説明しかなくてあまりC++に対して言及しているものが少なく感じます。

ただ、やはり自分としてはできるだけコードを書いて作っていきたいので、UE4のC++入門などを通して得た内容をメモとして書いていこうと思います。

新規プロジェクト作成

まずは、なにはなくともプロジェクトを作らないと始まりません。
ということで、プロジェクトを作成します。
今回はイチからVR関連の操作を行うまでをやるので、空のプロジェクトから開始してみます。

f:id:edo_m18:20180207111037p:plain

C++ファイルを追加する

さて、プロジェクトが出来たのでまずはC++のファイルを作成してみます。
「Contents Browser」の「Add New」から、「New C++ Class...」を選択します。

※ ちなみに、エディタの言語を英語にしています。なぜなら、コンポーネントの検索などに日本語でしか反応しないコンポーネントとかもあって作業しづらいからです。

f:id:edo_m18:20180207110057p:plain

続くウィンドウで、Pawnをベースクラスとして作成します。

f:id:edo_m18:20180207110431p:plain

雛形を見てみる

さて、生成されたcppファイルを見てみると以下のように、すでにある程度雛形になった状態になっています。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "VRPawn.generated.h"

UCLASS()
class MYPROJECT_API AVRPawn : public APawn
{
    GENERATED_BODY()

public:
    // Sets default values for this pawn's properties
    AVRPawn();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    // Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};

Unityの新規作成したC#コードと見比べても、あまり大きくは違いませんね。
Unityのものと同様、スタート時点で呼ばれるメソッド、アップデートごとに呼ばれるメソッドが最初から定義されています。

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:   
    // Called every frame
    virtual void Tick(float DeltaTime) override;

この部分ですね。
使い方もUnityのものとほぼ同様です。

ただ、少し異なる点としては、毎フレームのメソッド呼び出し(Tick)が必要かどうかをboolで指定する点でしょうか。

// Sets default values
AVRPawn::AVRPawn()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
}

Updateごとにメソッドを呼ばれないようにするためには、このフラグをfalseにする必要があります。

マクロなどを見てみる

少しだけ脱線して、雛形に最初から挿入されているマクロなどをちょっとだけ覗いてみました。

// GENERATED_BODY
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);

#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)

#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D

// UCLASS
#if UE_BUILD_DOCS || defined(__INTELLISENSE__ )
#define UCLASS(...)
#else
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#endif

ふむ。よく分からんw
ただ、様々な機能をブループリントで利用するためだったりで、いたるところでマクロが使われているので、細かいところは後々追っていきたい。
ひとまずは以下の記事が参考になりそうです。

qiita.com

エディタでプロパティを表示させる

さて、話を戻して。

Unityのインスペクタに表示するのと似た機能がUEにもあります。
それがUPROPERTYマクロです。以下のように指定します。

public:
    UPROPERTY(EditAnywhere) int32 Damage;

UPROPERTY(EditAnywhere)を、エディタに公開したいメンバに追加するだけですね。

コンパイルすると以下のように、エディタ上にパラメータが表示されるようになります。

f:id:edo_m18:20180207104916p:plain

参考:

docs.unrealengine.com

docs.unrealengine.com

さて、以上でC++を書き始める準備が整いました。

ここからは実際の開発で使いそうな部分を含めつつ、VRのモーションコントローラを使うまでを書いていきたいと思います。

C++コードを書き始める

さぁ、ここから実際にC++コードを書いて、VRモーションコントローラを操作できるまでを書いていきたいと思います。

ログを出力する

まず始めに、現状を知る上で必要なログ出力から。
UEでログを出力するには以下のようにしります。

UE_LOG(LogTemp, Log, TEXT("hoge"));

また、オブジェクト名などを出力するには以下のようにします。

AActor *actor = GetOwner();
if (actor != nullptr)
{
    FString name = actor->GetName();
    UE_LOG(LogTemp, Log, TEXT("Name: %s"), *name);
}

TEXTに、引数を指定できるようにしつつ、*nameとして変数を指定します。

TEXTには、FStringではなくTCHAR*を指定しないとならないようです。
そのため、*FStringとして変換する必要があります。

変換関連については以下のTipsが詳しいです。

qiita.com

ログは、「アウトプットログ」ウィンドウに表示されます。

画面にログを表示する

コンソールだけでなく、ゲーム画面にもログを出力することができるようになっています。
画面へのログ出力にはGEngineを使うため、Engine.hをincludeする必要があります。

#include "Engine.h"

ログを出力するには以下のようにGEngine->AddOnScreenDebugMessageを利用します。

if (GEngine)
{
    GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, FString::Printf(TEXT("hoge")));
}

引数などの細かな項目はこちらの記事が詳しいです。

increment.hatenablog.com

MotionControllerComponentを使う

Oculusなどのモーションコントローラ(Oculus Touchなどのコントローラ)をC++から利用するには、適切なヘッダファイルなどのセットアップが必要となります。

ヘッダをinicludeする

まず、UMotionControllerComponentを利用するためにはMotionControllerComponent.hをincludeする必要があります。

ビルド設定にモジュールを設定する

UEに不慣れなので、地味にハマったところがここ。
どうやらUEでは、特定のC#スクリプトでビルド周りのセットアップなどが実行されているようで、それに対して適切に、利用するモジュールを伝えてやる必要があるようです。

具体的には、PROJECT_NAME.Build.csに適切に依存関係を設定してやる必要があります。
該当のファイルは、C++ファイルが生成されたフォルダに自動的に作られています。

e.g.) MyProjectというプロジェクトを作成した場合はMyProject/Source以下にあります。

該当のCSファイルに、以下のようにモジュール名を指定します。

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class MyProject : ModuleRules
{
    public MyProject(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

        PrivateDependencyModuleNames.AddRange(new string[] { "HeadMountedDisplay" , "SteamVR" });

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

デフォルトで生成されていた部分からの差分は以下の部分です。

PrivateDependencyModuleNames.AddRange(new string[] { "HeadMountedDisplay" , "SteamVR" });

です。
プロジェクト生成直後はここが空の配列になっているので、ここに依存関係を追加してやる必要があります。(HeadMountedDisplaySteamVRのふたつ)

これで無事、エラーが出ずにコンパイルすることができるようになります。

逆に、この設定をしていないと以下のようなエラーが出力されてコンパイルに失敗します。

error LNK2019: unresolved external symbol "private: static class UClass * __cdecl UMotionControllerComponent::GetPrivateStaticClass(void)" (?GetPrivateStaticClass@UMotionControllerComponent@@CAPEAVUClass@@XZ) referenced in function "public: __cdecl AVRPawn::AVRPawn(void)" (??0AVRPawn@@QEAA@XZ)

以上を踏まえた上で、最後、MotionControllerComponentをセットアップしていきます。

ということで、UMotionControllerComponentC++から設定するサンプルコード。

まずはヘッダ。

// VRPawn.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "MotionControllerComponent.h"
#include "VRPawn.generated.h"

UCLASS()
class MYPROJECT_API AVRPawn : public APawn
{
    GENERATED_BODY()

private:
    UMotionControllerComponent *_leftMotionController;
    UMotionControllerComponent *_rightMotionController;

public:
    // Sets default values for this pawn's properties
    AVRPawn();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    // Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};

実装は以下になります。

// VRPawn.cpp

#include "VRPawn.h"
#include "Engine.h"

AVRPawn::AVRPawn()
{
    PrimaryActorTick.bCanEverTick = true;
    USceneComponent *root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    RootComponent = root;

    _leftMotionController = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("MotionControllerLeft"));
    _rightMotionController = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("MotionControllerRight"));
}

void AVRPawn::BeginPlay()
{
    Super::BeginPlay();
}

void AVRPawn::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void AVRPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
}

簡単なセットアップコードは以上です。

この状態でコンパイルし、シーンに配置すると以下のような状態になっていれば成功です。

f:id:edo_m18:20180207171131p:plain

これで、あとはOculusなどを起動するとOculus Touchなどの動きを反映してくれるようになります。

ただ、現状の状態はあくまでプログラム上認識させ、位置をアップデートする、というところまでなので、(当然ですが)なにかしらのモデル(メッシュ)を配置してあげないと動いていることが確認できないので、適当に好きなモデルなどを配置してください。

コンポーネントの概念

最後に少しだけコンポーネントの概念をメモっておきます。

最初、Unityのコンポーネントと同義だと思っていたので色々「?」になる状況があったのですが、少しだけUnityとは概念が違うようです。

大きな違いは、コンポーネントも階層構造を持てるということ。
Unityでは普通、GameObjectひとつに対してコンポーネントを追加していくように作成していくかと思います。

しかし、コンポーネント自体はネスト構造を持つことはできません。
もし持たせたい場合は、新しくGameObjectを作り、GameObject自体の入れ子を元にコンポーネントを配置していくことになるかと思います。

UEではコンポーネント自体にネスト構造を持たせることができるため、(自分のイメージでは)Unityのコンポーネントよりもより実体に近い形でインスタンス化されるもの、という認識です。

誤解を恐れずに言うと、UnityのGameObjectに、なにかひとつのコンポーネントを付けた(機能を持った)オブジェクト=UEのコンポーネント、という感じです。

こう把握してから一気に色々と理解が進んだのでメモとして残しておきます。

ハマった点

VRモードで動かすまでにいくつかハマった点があったのでメモ。

VRカメラの位置が指定した位置から開始しない

VRコンテンツ制作がメインなので、当然UEもそれを目的として始めました。
最初のごく簡単なプロジェクトを作成して、いざVRモードで見てみたらなぜかシーンビューのカメラ位置から開始されるという問題が。

結論から言うと、PawnのプロパティであるAuto Process PlayerDisableからPlayer0に変更することで直りました。
UEのドキュメントに載っているやつと載ってないのがあったので、地味にハマりました・・・。

便利拡張

UnityでC#書いているときは、///でコメントを、XMLのフォーマットで自動挿入してくれていたんですが、C++だとそれができないなーと思っていたら、拡張でまったく同じことをしてくれるものがありました。

jyn.jp


余談

ここからはVRコンテンツ開発とは全然関係なくなるので、完全に余談です。
公開されているUnreal Engine自体のソースコードからビルドしたエディタを使おうとして少しだけハマった点をメモ。

UE4を使うメリットのひとつとして、エンジンのソースコードが公開されていることが上げられます。
エンジンのソースコードを読むと、中でどういった処理がされているか、といったことを学ぶのに役立ちます。
またそれを見ておけば、もしエンジンのバグに遭遇したときにもそれを対処する方法が見つかるかもしれません。

ということで、ソースコードからビルドしてUE4を使ってみようと思ってやったところ、いくつか(環境依存の)問題が出てきたので、それをメモとして残しておこうと思います。

Visual Studioのセットアップでエラー

最初、UEをビルドするに当たって、まずプロジェクトファイルを生成する必要があります。
しかし、そのセットアップ用のバッチファイルを実行したところ、以下のようなエラーが表示されました。

ERROR: No 32-bit compiler toolchain found in C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\cl.exe

調べていくと、Visual C++ Toolsがインストールされていないことが原因のよう。

f:id:edo_m18:20180204165901p:plain

ということで、さっそくインストール・・・と思ったら、セットアップ時にクラッシュするじゃありませんか。

まずはクラッシュした理由をさぐるべく、クラッシュログの場所を探す。
場所はMSDNFAQ:Where are the setup logs stored ?)に掲載されていました。

場所は%Temp%とのこと。
(自分の環境だとC:\Users\[USER_NAME]\AppData\Local\Temp

似たような質問も上がっていました。

Setup detected an issue during the operation. Please click below to check for a solution and help us to improve the setup experience. | Microsoft Developer

フォントが悪さをしていた

色々調べていくと、どうやらフォントが悪さをしているということのよう。
どうも、VSはフォントと相性が悪いらしく、Windows Presentation Foundation Font Cacheというサービスが起動しているとクラッシュするらしい。

ということで、さっそくそれを停止してみたものの、セットアップでのクラッシュは直らず。
どこかで見かけた(記事のURLがわからないんですが・・)、フォントをいったん別の場所にバックアップしておき、消せるやつだけすべて消してからやるとうまくいくよ、というのを思い出してやってみたところ、うまく行きました。

なので、もし似た現象で困っている人がいたら、フォントを消せるだけすべて消してみて(いくつかはワーニングが出て消せない)リトライしてみるとうまく行くかもしれません。

参考にした記事

qiita.com

docs.unrealengine.com

docs.unrealengine.com

UnityのCompute ShaderでCurl Noiseを実装(流体編)

概要

今回は「流体編」と題しました。
というのも、発表されたカールノイズ自体には剛体との当たり判定を行う部分の記載もあるのですが、そこがうまく動いてないのでまずは流体部分の説明にフォーカスして書いていこうと思います。


Curl Noice。なんとなく流体っぽく見えるノイズ。
Curlとは「カール」、つまり「渦巻く」という意味からつけられた名前だそう。

(自分の浅い知識ながら)もう少し具体的に言うと、ベクトル場を回転(Curl)させることで作るノイズ、ということで「Curl Noise」というと思っています。

いつもお世話になっている物理のかぎしっぽさんで、この「ベクトル場の回転」についての記載があります。

ちなみに今回の主な実装は以下の記事を参考にさせていただきました。

qiita.com

また、発表された論文はこちら(PDF)

ソースコード

今回のサンプルはGithub上にUnityプロジェクトとしてアップしてあります。興味がある方はDLして見てみてください。

github.com

(自分の顔アイコンになっちゃうのどうにかならんか・・)

今回の記事で書くこと

今回の記事は、主にCurl Noiseについて触れます。
サンプルはComputeShaderを使ってUnityで実装しました。ComputeShaderによる実装についても少し触れます。

ただ、ComputeShaderなどの使い方などは知ってる前提であまり詳しくは解説しません。
ComputeShader自体については前の記事を読むなどしてください。

大まかな解説

自分はあまり物理に明るくありませんが、今回の実装を通して色々と学びがあったので、それをまとめる意味でも少しだけ解説を書きたいと思います。
(注) あくまで個人的な理解なので、間違っている可能性があります。

非圧縮性を確保する

流体が流体っぽく見える所以は、この非圧縮性が必要だという認識です。
非圧縮性の説明をWikipediaから引用させてもらうと、

非圧縮性流れ(ひあっしゅくせいながれ)とは流体力学において、流体粒子の内部で密度が一定の流体である。縮まない流体とも呼ばれる[1][2]。連続体力学における非圧縮性の概念を流体に適用したものである。
言い換えると、非圧縮性とは流体の速度の発散が 0 になることである(この表現が等価である理由は後述)。

とのこと。
ここで、流体の速度の発散が0になることであるとあります。

物理のかぎしっぽさんの記事のdivから、「発散」についての説明を引用させてもらうと、

スカラー場の勾配を考えたとき,ベクトル微分演算子  \nabla = \left(\frac{\partial}{\partial x_{1}}, \frac{\partial}{\partial x_{2}}, \frac{\partial}{\partial x_{3}}\right)というものを導入しました.そして,この  \nablaスカラー関数に作用させたものを勾配(  {\rm grad} )と呼びました.  \nabla をベクトル関数と内積を取る形で作用させたものを 発散 と呼びます.英語で発散を divergence と言うので,記号  {\rm div} を使う場合もあります.

さらに別の箇所から引用すると、

ベクトル場を,水(非圧縮流体)の流れだと考えると状況がイメージしやすいでしょう.この直方体領域は流れの中に置かれていますから,絶えず水が流れ込んだり出て行ったりしています.しかし,水は非圧縮流体だと仮定していますので,普通なら,入ってくる水量と出て行く水量は同じはずです.

と書かれています。
つまり、(自分の理解では)この「発散が0になる」とは、流れの中で流入と流出が等しく起こる=差分がゼロ、ということだと思います。

そしてカールノイズはこの「発散0を実現してくれるための手法」ということのようです。
理由として自分が理解しているのは、以下のように考えています。
まず言葉で説明すると、

あるベクトル場から得たベクトルを「回転」させ、「内積」を取った結果がゼロである。

ということ。式にすると、


\nabla \cdot \nabla \times \vec{v} ≡ 0

カール(回転)は、どうやら90°回転させることのようです。
つまり、「とあるベクトルを90°回転させたベクトルとの内積=0」ということです。
そもそも内積は、垂直なベクトルとの計算は0になりますよね。

なので、ある意味0になるのは自明だった、というわけです。(という認識です)

発散の定義が、 \nablaとの内積を取ったものが 0になる、ということなので、「だったら最初からゼロになるように回転しておこうぜ」というのが発想の基点なのかなと理解しています。

実際にUnity上で実行した動画はこんな感じ↓

パーティクルの位置の計算についてはComputeShaderを使って計算しています。
ComputeShaderについて前回2回に分けて記事を書いているので、よかったら見てみてください。

edom18.hateblo.jp

edom18.hateblo.jp

それから、描画周りの実装については、凹みTipsで過去に凹みさんが書かれていた以下の記事を参考に実装しています。
(というかほぼそのままです)
いつもありがとうございます。

tips.hecomi.com

用語解説

いくつか数学・物理的なワードが出てくるので、簡単にまとめておきます。

ベクトル場

ものすごくざっくりいうと、「ベクトル」がちらばった空間です。
分かりやすい例で言うと、風速ベクトルを空間にマッピングしたもの、と考えるといいかもしれません。

風速はその場所その場所で当然向きや強さが違います。
そしてその瞬間の風速ベクトルを記録したのがベクトル場、という感じです。

プログラム的に言うと、ベクトルを引数に取ってベクトルを返す関数、と見ることが出来ます。 (厳密に言うと風速は時間によっても変化するので、時間と位置による関数となります)

float3 windVec = GetWind(pos, time);

みたいな感じですね。
今回のCurlNoiseについては時間的変化は考えず、以下のように速度ベクトルを求めています。

float3 x = _Particles[id].position;
float3 velocity = CurlNoise(x);

ベクトル場については、物理のかぎしっぽさんのところにとても分かりやすい例と図があるので、そちらを見てみてください。

カール(Curl・rot)

ベクトル場の回転を表す。
前述の、物理のかぎしっぽさんの記事から引用させてもらうと、

三次元ベクトルの場合,ナブラを 外積を取る形で作用させる ことも可能です.これを 回転 と呼び,  \nabla \times, rot もしくは curl という記号で表現します.この記事では,回転について基本的な意味や性質を考えます.

ramp関数

ramp関数 | Wikipediaとは、グラフが斜路(ramp)に見えることから名付けられたそう。

論文で書かれている式をグラフにしてみると以下の形になります。
なんとなく「斜路」という意味が分かりますね。

f:id:edo_m18:20180117234619p:plain

本題

さて、ここからが、今回実装した内容の解説です。
まず、Curl Noiseについてざっと解説してみます。(自分もまだ理解が浅いですが)

前述のように、ベクトル場を(ベクトル解析でいう)回転させることによって得られるノイズを「Curl Noise」と呼んでいるようです。
元々は、アーティスティックな流体表現を実現するために考案されたもののようです。

物理のかぎしっぽさんの記事を参考に式を表すと以下のようになります。

f:id:edo_m18:20171014165055p:plain

 \nablaは「ナブラ」と読み、「微分演算子をベクトルに組み合わせたナブラというベクトル演算子」ということのようです。

記号を見ると偏微分の記号なので、要は、ベクトル場の各ベクトルに対してちょっとずつ位置をずらして計算したもの、と見ることができます。

CurlNoiseを生成している関数を見てみると、以下のようになります。

float3 CurlNoise(Particle p)
{
    const float e = 0.0009765625;
    const float e2 = 2.0 * e;
    const float invE2 = 1.0 / e2;

    const float3 dx = float3(e, 0.0, 0.0);
    const float3 dy = float3(0.0, e, 0.0);
    const float3 dz = float3(0.0, 0.0, e);

    float3 pos = p.position;

    float3 p_x0 = SamplePotential(pos - dx, p.time);
    float3 p_x1 = SamplePotential(pos + dx, p.time);
    float3 p_y0 = SamplePotential(pos - dy, p.time);
    float3 p_y1 = SamplePotential(pos + dy, p.time);
    float3 p_z0 = SamplePotential(pos - dz, p.time);
    float3 p_z1 = SamplePotential(pos + dz, p.time);

    float x = (p_y1.z - p_y0.z) - (p_z1.y - p_z0.y);
    float y = (p_z1.x - p_z0.x) - (p_x1.z - p_x0.z);
    float z = (p_x1.y - p_x0.y) - (p_y1.x - p_y0.x);

    return float3(x, y, z) * invE2;
}

eが、「ちょびっと動かす」部分の値で、それを各軸に対して少しずつ変化させる量としてdxdydzを定義しています。
そしてそれぞれ変化した値をSamplePotential関数に渡し、さらにそれの差分を取ることで計算を行っています。

ベクトル場の回転について、論文に書かれているものは以下です。


\vec{v}(x, y, z) =
\left(
\frac{∂\psi_3}{∂ y} − \frac{∂\psi_2}{∂ z}, \frac{∂\psi_1}{∂ z} − \frac{∂\psi_3}{∂ x}, \frac{∂\psi_2}{∂ x} − \frac{∂\psi_1}{∂ y}
\right)

ベクトル微分演算子を作用させるので、微分を取る目的で以下のように計算しています。

float3 p_x0 = SamplePotential(pos - dx, p.time);
float3 p_x1 = SamplePotential(pos + dx, p.time);
float3 p_y0 = SamplePotential(pos - dy, p.time);
float3 p_y1 = SamplePotential(pos + dy, p.time);
float3 p_z0 = SamplePotential(pos - dz, p.time);
float3 p_z1 = SamplePotential(pos + dz, p.time);

それぞれの方向(x, y, z)に対してちょっとずつずらした値を取得しているわけですね。
そして続く計算で、前述の「ベクトル場の回転」を計算します。

float x = (p_y1.z - p_y0.z) - (p_z1.y - p_z0.y);
float y = (p_z1.x - p_z0.x) - (p_x1.z - p_x0.z);
float z = (p_x1.y - p_x0.y) - (p_y1.x - p_y0.x);

[2018.06.29追記]
すみません、計算ミスをしていました。以前は以下のように書いていましたが、最後のパラメータの微分の計算が「マイナス」になるのが正解です。

// 間違っていた記述
float x = (p_y1.z - p_y0.z) - (p_z1.y + p_z0.y);
float y = (p_z1.x - p_z0.x) - (p_x1.z + p_x0.z);
float z = (p_x1.y - p_x0.y) - (p_y1.x + p_y0.x);

↑カッコでくくった右側の計算が「プラス」になっていました。

前述の数式と見比べてもらうと同じことを行っているのが分かるかと思います。

誤解を恐れずに言えば、カールノイズのコア部分はここだけです。
つまり、

  1. ベクトル場からベクトルを得る
  2. 得たベクトルを回転させる
  3. 得たベクトルを速度ベクトルとして計算する
  4. 速度ベクトルから次の位置を決定する

だけですね。

あとはこの、算出された回転済みのベクトル(速度)をパーティクルに与えるだけで、なんとなく流体の流れっぽい動きをしてくれるようになります。

ノイズを生成する

さて、カールノイズをもう少し具体的に見ていきます。
カール「ノイズ」というくらいなので、当然ノイズを生成する必要があります。

色々なカールノイズのサンプルを見ると、シンプレクスノイズかパーリンノイズを利用しているケースがほとんどです。
論文でもパーリンノイズに言及しているので、今回はパーリンノイズを利用してカールノイズを作成しました。

ノイズを生成する上で、論文には以下のように書かれています。

To construct a randomly varying velocity field we use Perlin noise  N(\vec{x}) in our potential. In 2D, ψ = N. In 3D we need three components for the potential: three apparently uncorrelated noise functions (a vector  vec{N}(\vec{x}) do the job, which in practice can be the same noise function evaluated at large offsets.

Note that if the noise function is based on the integer lattice and smoothly varies in the range [−1,1], then the partial derivatives of the scaled N(x/L) will vary over a length-scale L with values approximately in the range O([−1/L,1/L]). This means we can expect vortices of diameter approximately L and speeds up to approximately O(1/L): the user may use this to scale the magnitude of ψ to get a desired speed.

ざっくり訳してみると、


ランダムな変化するベクトル場を構築するのに、パーリンノイズ( \vec{x})をポテンシャルのために利用する。2Dの場合は「 \psi = N」。

3Dの場合はポテンシャルのために3つの要素が必要となる。
3つの、明らかに無関係なノイズ関数( \vec{N}(\vec{x})を使用する。それらは同じノイズ関数を、大きなオフセットを持たせたもの。

Note: もし、ノイズ関数が整数格子にもとづいており、[-1, 1]の範囲で滑らかに変化するのであれば、 N(x/L)にスケーリングされた偏微分は、おおよそO([−1/L,1/L])の範囲の値でスケールLに渡って変化する。

これは、ほぼ直径Lの渦とO(1/L)までの速度を期待することができることを意味する。ユーザは \psiの値にスケールされた(望みの)スピードを使えるかもしれない。


ということで、ノイズとスケールを考慮した処理は以下のようになります。

float3 SamplePotential(float3 pos)
{
    float3 s = pos / _NoiseScales[0];
    return Pnoise(s);
}

_NoiseSclaesは、いくつかのスケール値が入った配列です。
が、今回はひとつのみ利用しているので_NoiseScales[0]だけを使っています。

Pnoiseはパーリンノイズを利用したベクトル場の計算です。
実装は論文に書かれているように、大きなオフセットを持たせた同一のパーリンノイズ関数を利用して3要素を計算しています。

// パーリンノイズによるベクトル場
// 3Dとして3要素を計算。
// それぞれのノイズは明らかに違う(極端に大きなオフセット)を持たせた値とする
float3 Pnoise(float3 vec)
{
    float x = PerlinNoise(vec);

    float y = PerlinNoise(float3(
        vec.y + 31.416,
        vec.z - 47.853,
        vec.x + 12.793
        ));

    float z = PerlinNoise(float3(
        vec.z - 233.145,
        vec.x - 113.408,
        vec.y - 185.31
        ));

    return float3(x, y, z);
}

ちなみにPerlinNoiseは以前、JavaScriptで実装したときのものをCompute Shaderに移植したもので、以下のように実装しています。

float PerlinNoise(float3 vec)
{
    float result = 0;
    float amp = 1.0;

    for (int i = 0; i < _Octaves; i++)
    {
        result += Noise(vec) * amp;
        vec *= 2.0;
        amp *= 0.5;
    }

    return result;
}

float Noise(float3 vec)
{
    int X = (int)floor(vec.x) & 255;
    int Y = (int)floor(vec.y) & 255;
    int Z = (int)floor(vec.z) & 255;

    vec.x -= floor(vec.x);
    vec.y -= floor(vec.y);
    vec.z -= floor(vec.z);

    float u = Fade(vec.x);
    float v = Fade(vec.y);
    float w = Fade(vec.z);

    int A, AA, AB, B, BA, BB;

    A = _P[X + 0] + Y; AA = _P[A] + Z; AB = _P[A + 1] + Z;
    B = _P[X + 1] + Y; BA = _P[B] + Z; BB = _P[B + 1] + Z;

    return Lerp(w, Lerp(v, Lerp(u, Grad(_P[AA + 0], vec.x + 0, vec.y + 0, vec.z + 0),
                                    Grad(_P[BA + 0], vec.x - 1, vec.y + 0, vec.z + 0)),
                            Lerp(u, Grad(_P[AB + 0], vec.x + 0, vec.y - 1, vec.z + 0),
                                    Grad(_P[BB + 0], vec.x - 1, vec.y - 1, vec.z + 0))),
                    Lerp(v, Lerp(u, Grad(_P[AA + 1], vec.x + 0, vec.y + 0, vec.z - 1),
                                    Grad(_P[BA + 1], vec.x - 1, vec.y + 0, vec.z - 1)),
                            Lerp(u, Grad(_P[AB + 1], vec.x + 0, vec.y - 1, vec.z - 1),
                                    Grad(_P[BB + 1], vec.x - 1, vec.y - 1, vec.z - 1))));
}

パーリンノイズ自体の詳しい内容は、調べるとたくさん記事が出てくるのでそちらをご覧ください。

カールノイズのカーネル

さて最後に、カールノイズを計算しているカーネル(Compute Shaderのmain関数のようなもの)を見てみます。

[numthreads(8, 1, 1)]
void CurlNoiseMain(uint id : SV_DispatchThreadID)
{
    float3 pos = _Particles[id].position;

    float3 velocity = CurlNoise(_Particles[id]);

    _Particles[id].velocity = velocity * _SpeedFactor * _CurlNoiseIntencity;
    _Particles[id].position += _Particles[id].velocity * _DeltaTime;

    _Particles[id].time += _DeltaTime;
    float scale = 1.0 - (_Particles[id].time / _Particles[id].lifeTime);
    if (scale < 0)
    {
        _Particles[id].active = false;
        return;
    }

    _Particles[id].scale = scale;
}

_Particlesがパーティクルの配列になっていて、Particle構造体がそれぞれのパーティクルの位置や速度などを保持しています。
それを、毎フレームごとに更新し、それをレンダリングすることで流体のような動きを実現している、というわけです。

現在リポジトリにアップしているものは、これに加えて、剛体との衝突判定の実装を試みているのでもう少し複雑な状態になっていますが、衝突判定なしで流体のような動きだけがほしい場合は以上の実装で実現することができます。

ガチの流体計算に比べるとだいぶ簡単な処理で実現できているのが分かってもらえたかと思います。

次回予告 - 衝突判定

さて、冒頭でも書きましたが、このカールノイズ。
簡単な衝突判定を利用して、球体に沿わしたり、といったことができます。

球体が干渉しているのが分かるかと思います。
衝突判定が取れるようになれば、さらに流体感を増した表現になるので、ぜひマスターしたいところです。

実行時に使えるユニークな識別子を生成する

概要

Unityで開発していると、ときに、「固定の」ユニークなID(識別子)を割り振りたいときがあります。
Unityにはもともと、Object.GetInstanceIDがありますが、これはユニークではあるものの、実行時や保存時などに書き換わる可能性があります。
(どのタイミングで変わるかはちゃんと調べてませんが、同じオブジェクトのIDが変わることがあるのを確認しています)

今回書くことは、各オブジェクトの固定IDです。
利用シーンとしては、オブジェクトごとのデータを保存してあとで復元する、などを想定しています。

具体例で言うと、シーンをまたいだときに、そのオブジェクトの位置を保存しておく、と言った用途です。

解決策

まず最初に、どうやって固定のIDを割り振るかを書いてしまうと、単純に、そのオブジェクトの「階層」を利用します。

つまり、OSのファイル管理と同様のことをやるってことですね。
各オブジェクトは必ず階層構造を持ち、シーンファイルに紐付いています。
つまり、シーンファイル名をルートとした階層構造を文字列化すれば、それはユニークなIDとなります。

一点だけ注意点として、OSであれば同名ファイル名は許されていませんが、Unityの場合は同階層に同じ名前のオブジェクトを配置することができます。
なので、少しだけ工夫して、最後に、Siblingのindexを付与することでユニークなIDができあがります。

最終的にはこのパスを元にした文字列のハッシュ値を保持して比較することで、ユニークなIDとして利用することができるようになります。

コード

コードで示すと以下のようになります。

// 以下は、「GameObjectUtility」クラスの静的メソッドとして定義していると想定。

/// <summary>
/// ヒエラルキーに応じたパスを取得する
/// </summary>
static public string GetHierarchyPath(GameObject target)
{
    string path = "";
    Transform current = target.transform;
    while (current != null)
    {
        // 同じ階層に同名のオブジェクトがある場合があるので、それを回避する
        int index = current.GetSiblingIndex();
        path = "/" + current.name + index + path;
        current = current.parent;
    }

    Scene belongScene = target.GetBelongsScene();

    return "/" + belongScene.name + path;
}

※ 上のコードのGetBelongsSceneは拡張メソッドで、以下のように実装しています。

using UnityEngine.SceneManagement;

public static class GameObjectExtension
{
    public static Scene GetBelongsScene(this GameObject target)
    {
        for (int i = 0; i < SceneManager.sceneCount; i++)
        {
            Scene scene = SceneManager.GetSceneAt(i);
            if (!scene.IsValid())
            {
                continue;
            }

            if (!scene.isLoaded)
            {
                continue;
            }
            
            GameObject[] roots = scene.GetRootGameObjects();
            foreach (var root in roots)
            {
                if (root == target.transform.root.gameObject)
                {
                    return scene;
                }
            }
        }

        return default(Scene);
    }
}

実際に使う際は、取得したパス文字列のハッシュを保持しておきます。

string id = GameObjectUtility.GetHierarchyPath(gameObject);
int hash = id.GetHashCode();

// hashをなにかしらで保存する

階層構造の変更に対応する

さて、上記までである程度固定のIDを振ることができますが、動的に、階層構造が変更される、あるいは子要素が追加されるなどは当然ながら発生します。

すると問題になるのが、実行順によって階層構造が変更され、完全なユニーク性が失われる、ということです。

なので、シーンファイルの保存タイミングをフックして、その際にSerializeFieldに保持してしまう、という方法でこれを解決します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;

/// <summary>
/// 保存時に、シーンに設定されているユニークID保持対象のPathを設定(ベイク)する
/// </summary>
public class SaveAllUniquePath : UnityEditor.AssetModificationProcessor
{
    /// <summary>
    /// アセットが保存される直前のイベント
    /// </summary>
    /// <param name="paths">保存される対象アセットのパス</param>
    static private string[] OnWillSaveAssets(string[] paths)
    {
        foreach (var path in paths)
        {
            Scene scene = SceneManager.GetSceneByPath(path);

            if (scene.IsValid())
            {
                GameObject[] roots = scene.GetRootGameObjects();
                foreach (var root in roots)
                {
                    RecursiveUpdateUniquePath(root.transform);
                }
            }
        }

        return paths;
    }

    /// <summary>
    /// 再帰的にUniquePathを更新する
    /// </summary>
    static void RecursiveUpdateUniquePath(Transform target)
    {
        UniquePathTarget upt = target.GetComponent<UniquePathTarget>();
        if (upt != null)
        {
            upt.SetUniquePathAndHash();
        }

        for (int i = 0; i < target.childCount; i++)
        {
            RecursiveUpdateUniquePath(target.GetChild(i));
        }
    }
}

以上です。
まぁぶっちゃけベイクしてしまうので、そもそもシーン名と連番やインスタンスIDでもそれなりに動くようにはできますが、実行順をちゃんと制御できるなら完全なランタイム時にIDを生成しても動くものが作れるのでメモとして書いてみました。

ランタイムでAvatarを生成してアニメーションに利用する

今日の記事は、UnityAdvent Calendarの9日目の記事です。

qiita.com

概要

VR開発をしていると、キャタクター(アバター)を表現する方法として頭と手だけの簡易的アバターか、通常のキャラクターモデルを用いたアバターの2種類から選ぶことになります。
そして後者、キャラクターモデルを利用したアバターを制作する際に利用できそうな方法を見つけたので、それのメモです。

具体的には、Avatarインスタンスをランタイム時に生成し、それをMecanimとしてキャラクターにアサインする方法となります。

ちなみに、今回の実装のヒントは、OptiTrackというモーションキャプチャーのシステム用に提供されているプラグインの中のソースコードを参考にしました。

プラグインはフリーで、以下からダウンロードすることができます。

optitrack.com

実際に使ってみたのがこちら。Avatarなのでアニメーションを簡単にコピー、複製することができます。

この記事のサンプルはGitHubに上がっています。(ただ、元のサンプルはVRIKを使っているため、その部分はコメントアウトしてあります。もし実際に動くものを見たい場合はご自身でVRIKを導入してご確認ください)

github.com

必要クラス

今回利用するクラスは以下です。
それぞれが、アバターを構成するための情報を保持、伝達するためのものです。

  • Avatar
  • AvatarBuilder
  • HumanBone
  • SkeltonBone
  • HumanDescription
  • HumanPose
  • HumanPoseHandler
  • HumanTrait

大まかな手順

大まかな手順は、(Avatarの設定を行ったことがある方であればイメージしやすいと思いますが)Configure Avatarボタンを押下して編集モードに入ったときにできることをプログラムから行う、というイメージです。

具体的には、HumanBoneSkeletonBoneを用いてスケルトンとモデルの構造を定義し、それらを関連付け、最後に関節の曲がり具合などを設定した「設定オブジェクト(HumanDescription)」とともに、Avatarを生成する、という形です。

この設定を行う際、適切にセットアップが終わっていないと最後のAvatarBuilderでビルドする段階でエラーが出てしまうので、セットアップは気をつける必要があります。

百聞は一見にしかず、ということで、まずは実際にコードを見てもらったほうがいいでしょう。

Avatarのセットアップコード

各種ボーンの設定と、アバターのビルドを行うメソッドの抜粋です。

/// <summary>
/// アバターのセットアップ
/// </summary>
private void Setup()
{
    // HumanBoneのためのリストを取得する
    string[] humanTraitBoneNames = HumanTrait.BoneName;

    List<HumanBone> humanBones = new List<HumanBone>(humanTraitBoneNames.Length);
    for (int i = 0; i < humanTraitBoneNames.Length; i++)
    {
        string humanBoneName = humanTraitBoneNames[i];
        Transform bone;
        if (_transformDefinision.TryGetValue(humanBoneName, out bone))
        {
            HumanBone humanBone = new HumanBone();
            humanBone.humanName = humanBoneName;
            humanBone.boneName = bone.name;
            humanBone.limit.useDefaultValues = true;

            humanBones.Add(humanBone);
        }
    }

    List<SkeletonBone> skeletonBones = new List<SkeletonBone>(_skeletonBones.Count);
    for (int i = 0; i < _skeletonBones.Count; i++)
    {
        Transform bone = _skeletonBones[i];

        SkeletonBone skelBone = new SkeletonBone();
        skelBone.name = bone.name;
        skelBone.position = bone.localPosition;
        skelBone.rotation = bone.localRotation;
        skelBone.scale = Vector3.one;

        skeletonBones.Add(skelBone);
    }

    // HumanDescription(関節の曲がり方などを定義した構造体)
    HumanDescription humanDesc = new HumanDescription();
    humanDesc.human = humanBones.ToArray();
    humanDesc.skeleton = skeletonBones.ToArray();

    humanDesc.upperArmTwist = 0.5f;
    humanDesc.lowerArmTwist = 0.5f;
    humanDesc.upperLegTwist = 0.5f;
    humanDesc.lowerLegTwist = 0.5f;
    humanDesc.armStretch = 0.05f;
    humanDesc.legStretch = 0.05f;
    humanDesc.feetSpacing = 0.0f;
    humanDesc.hasTranslationDoF = false;

    // アバターオブジェクトをビルド
    _srcAvatar = AvatarBuilder.BuildHumanAvatar(gameObject, humanDesc);

    if (!_srcAvatar.isValid || !_srcAvatar.isHuman)
    {
        Debug.LogError("setup error");
        return;
    }

    _srchandler = new HumanPoseHandler(_srcAvatar, transform);
    _destHandler = new HumanPoseHandler(_destAvatar, _targetAnimator.transform);

    _initialized = true;
}

ボーンのセットアップ

まず冒頭で行っているのが、ボーンのセットアップです。
ボーンには2種類あり、HumanBoneSkeletonBoneの2種類です。

「人間の構造」を定義する「HumanBone」と実際の「SkeletonBone」

あくまで自分の理解で、という前置きが入りますが、HumanBone人間の構造を定義するためのボーンです。
そして実際のモデル(アバターに適用するオブジェクト)のボーン構造を示すのがSkeletonBoneです。

なぜこのふたつのボーン情報が必要なのかというと、モデルの中身を見たことがある人であればすぐにピンと来ると思いますが、モデルデータには人間にはないボーンが仕込まれている場合があります。
MMDなどは特にそれが顕著で、「よりよく見せるため」のボーンが仕込まれていたりします。
(例えばスカート用のボーンだったり、髪の毛用のボーンだったり)

そのため、人間の構造と同じ構造でボーンを定義することはほとんどなく、いくらかのボーンが人間の構造とは違った形になっているため、「実際のボーン構造のうち、どれが人間の構造としてのボーンか」を定義する必要がある、というわけです。(と理解しています)

そして、その関連付けを行っているのが、それぞれnameプロパティで指定される名称です。
どうやらUnity内では名称でそのマッチングを行っているようです。

なので、「人間としてのこのボーンは、対象モデルではこういう名称ですよ」という関連付けが必要、というわけですね。

コードとしては以下の部分ですね。

humanBone.boneName = bone.name;

// ... 中略 ...

skelBone.name = bone.name;

そして、「人間としてのどこのボーンか」という情報はhumanBone.humanName = humanBoneName;で指定しています。

こうして、人間としてのボーンがどれか、というマッチングを行うことで、Mecanimではその情報を元にアニメーションしている、というわけのようです。

最初、SkeletonBoneが人間のボーン構造を示すものだと思って、それだけのTransformを指定して配列を生成していたんですが、「hoge Transformは fuga Transformの親じゃないとダメだよ」みたいなエラーが出て、少しハマりました。
今回の実装ではSkeletonBoneは以下のように、ルートから再帰的にTransform情報を拾って配列化して設定しています。

/// <summary>
/// 再帰的にTransformを走査して、ボーン構造を生成する
/// </summary>
/// <param name="current">現在のTransform</param>
private void RecursiveSkeleton(Transform current, ref List<Transform> skeletons)
{
    skeletons.Add(current);

    for (int i = 0; i < current.childCount; i++)
    {
        Transform child = current.GetChild(i);
        RecursiveSkeleton(child, ref skeletons);
    }
}

Unityが規定した名称を基に各ボーンの関連付けを行う

さて、もうひとつ重要なのがこの「Unityが規定した名称を基に関連付けを行う」という点です。
どういうことかというと、まずは以下のコードを見てください。

string[] humanTraitBoneNames = HumanTrait.BoneName;

List<HumanBone> humanBones = new List<HumanBone>(humanTraitBoneNames.Length);
for (int i = 0; i < humanTraitBoneNames.Length; i++)
{
    string humanBoneName = humanTraitBoneNames[i];
    Transform bone;
    if (_transformDefinision.TryGetValue(humanBoneName, out bone))
    {
        HumanBone humanBone = new HumanBone();
        humanBone.humanName = humanBoneName;
        humanBone.boneName = bone.name;
        humanBone.limit.useDefaultValues = true;

        humanBones.Add(humanBone);
    }
}

HumanTrait.BoneNameというstring型の配列から値を取り出し、それと、自分が定義した_transformDefinisionの中に値が含まれているかのチェックをしています。

このHumanTrait.BoneNameが、Unityが規定しているボーンの名称で、具体的にはNeckなどの人体の部位の名称が設定されています。

そしてここで定義されている名称とのマッピングを行っているのが_transformDefinisionなのです。

これ自体はシンプルに、インスペクタから手で設定してもらったTransformを設定しているだけです。
生成部分は以下のようになります。

/// <summary>
/// アサインされたTransformからボーンのリストをセットアップする
/// </summary>
private void SetupBones()
{
    _transformDefinision.Clear();

    _transformDefinision.Add("Hips", _hips);
    _transformDefinision.Add("Spine", _spine);
    _transformDefinision.Add("Chest", _chest);
    _transformDefinision.Add("Neck", _neck);
    _transformDefinision.Add("Head", _head);
    _transformDefinision.Add("LeftShoulder", _leftShoulder);
    _transformDefinision.Add("LeftUpperArm", _leftUpperArm);
    _transformDefinision.Add("LeftLowerArm", _leftLowerArm);
    _transformDefinision.Add("LeftHand", _leftHand);
    _transformDefinision.Add("RightShoulder", _rightShoulder);
    _transformDefinision.Add("RightUpperArm", _rightUpperArm);
    _transformDefinision.Add("RightLowerArm", _rightLowerArm);
    _transformDefinision.Add("RightHand", _rightHand);
    _transformDefinision.Add("LeftUpperLeg", _leftUpperLeg);
    _transformDefinision.Add("LeftLowerLeg", _leftLowerLeg);
    _transformDefinision.Add("LeftFoot", _leftFoot);
    _transformDefinision.Add("RightUpperLeg", _rightUpperLeg);
    _transformDefinision.Add("RightLowerLeg", _rightLowerLeg);
    _transformDefinision.Add("RightFoot", _rightFoot);
    _transformDefinision.Add("LeftToes", _leftToes);
    _transformDefinision.Add("RightToes", _rightToes);
}

あとは、この設定されたリストとマッチングして、該当のボーンの名称を、前述のように設定していく、という感じになります。
再掲すると以下の部分です。

humanBone.boneName = bone.name;

// ... 中略 ...

skelBone.name = bone.name;

人間の特性を定義する「HumanDescription」

ボーンのセットアップが終わったら、そのボーン構造を持つ人の特性がどんなものか、を定義する「HumanDescription」構造体を利用して、手の関節の回転などの状態を定義します。

HumanDescription humanDesc = new HumanDescription();
humanDesc.human = humanBones.ToArray();
humanDesc.skeleton = skeletonBones.ToArray();

humanDesc.upperArmTwist = 0.5f;
humanDesc.lowerArmTwist = 0.5f;
humanDesc.upperLegTwist = 0.5f;
humanDesc.lowerLegTwist = 0.5f;
humanDesc.armStretch = 0.05f;
humanDesc.legStretch = 0.05f;
humanDesc.feetSpacing = 0.0f;
humanDesc.hasTranslationDoF = false;

アバターをビルド

以上で必要なデータが揃いました。
あとは、そのデータを利用して、アバターオブジェクトをビルドしてやれば完了です。

_srcAvatar = AvatarBuilder.BuildHumanAvatar(gameObject, humanDesc);

if (!_srcAvatar.isValid || !_srcAvatar.isHuman)
{
    Debug.LogError("setup error");
    return;
}

注意点として、ビルド後にエラーがないかのチェックが必要です。

上でも書きましたが、ボーンの構造などの状態がおかしいと、この時点でエラーが表示されます。
構造として適切でない場合はビルド時にエラーが出るのと同時に、isValidisHumanのフラグがfalseになるので、それをチェックして、失敗していた場合はやり直すなどの処置が必要になります。
(最初はここでエラーが出て、若干ハマった)

ただ、エラーを見てみるとしっかりと理由が書かれているので、それを元に修正していけば問題は解決できると思います。

無事ビルドが終わったら、HumanPoseHandlerを設定し、以後はUpdateメソッド内でアバターの状態をコピーしてやれば完成です。

HumanPoseHandlerは、現在の状態を取得するためのハンドラ。

_srchandler = new HumanPoseHandler(_srcAvatar, transform);
_destHandler = new HumanPoseHandler(_destAvatar, _targetAnimator.transform);

上記で取得したハンドラを用いてGet/Setメソッドを用いて、Getしたポーズを、以下のようにして対象のアバターにコピーします。

private void Update()
{
    if (!_initialized)
    {
        return;
    }

    if (_srchandler != null && _destHandler != null)
    {
        _srchandler.GetHumanPose(ref _humanPose);
        _destHandler.SetHumanPose(ref _humanPose);
    }
}

GetHumanPoseメソッドでポーズ情報を取得し、SetHumanPoseでコピー先のHumanPoseHandlerにセットしてやれば動きが同期されるようになります。

ちなみに、感のいい方であればピンと来ているかもしれませんが、このコピー先を複数用意してやれば、いくらでも同じ動きをするモデルを用意することができます。
Mecanimによるアバター制御の恩恵が受けられる、というわけですね。

f:id:edo_m18:20170923153401p:plain

最後に

冒頭で載せた動画は、このアバターの仕組みを使って「アバターの現在のアニメーション状態をコピー」することで実現しています。

Avatarをランタイムで生成できることで、色々な値を調整することが可能になるのでVRには非常に適したものかなと思っています。

ARKitで撮影した映像を(疑似)IBLとして利用する

今日の記事は、ARKitAdvent Calendarの2日目の記事です。

qiita.com

概要

今回は、ARKitで平面検出を行っている映像データを使って、(疑似)IBLをしてみたいと思います。

ちなみに、ARKitをUnityで使う際の実装については前の記事で少し書いたので、ARKit?って人はそっちも読んでみてください。

edom18.hateblo.jp

ARで3Dモデルを表示するととても面白いしテンション上がりますが、やはりどこか浮いて見えてしまいます。
というのも、人間は立体感を「影・陰」から判断しているため、光の当たり方が少し違うだけで違和感が出てしまうのです。

そして当然ですが、なにもしなければ3Dモデルを照らすライトはビルドしたときに用意したライトのみになります。

しかしARに関わらず、Skyboxのテクセルを光源とみなす、いわゆる「グローバルイルミネーション」の機能を使えば、映像からライティングが可能となります。

今回の趣旨は、ARKitが利用している映像を利用して、疑似IBLを実現してみよう、という内容になります。
なので厳密には、IBL自体を自前で実装したわけではなく、あくまで疑似IBLです。

実際に適用した図が以下になります↓

考え方

考え方はシンプルです。

  • ARKitで利用される環境のテクスチャを取得する
  • 取得したテクスチャを適度にぼかす(*1)
  • ぼかしたテクスチャを、全天球に貼り付ける
  • 全天球の中心に置いた専用カメラでCubeMapにレンダリングする
  • 生成したCubeMapから色をフェッチしてブレンド

という手順です。

*1 ぼかす理由は、IBL自体がそもそも、描画する点から複数方向に向かってサンプルRayを飛ばし、その色を合成することで得られます。
それは、環境光が全方面(点を中心とした半球状の方向)から到達するためであり、それをシミュレーションするために様々な方向の光をサンプリングするわけです。
そしてそれを擬似的に、かつ簡易的に実現する方法として「ぼかし」を利用しているわけです。

以前書いたPBRについての記事も参考になると思います。(光のサンプリングという点で)

qiita.com

ARKitのテクスチャからぼかしテクスチャを得る

今回の目的達成のために、若干、ARKitのプラグインUnityARVideoクラスのコードを編集しました。

_unityARVideo.VideoTextureY;
_unityARVideo.VideoTextureCbCr;

本来はVideTextureがprivateなフィールドのためアクセスできませんが、IBL用にpublicにして取得できるようにしてあります。

実際にブラーを施している箇所は以下のようになります。

for (int i = 0; i < _renerList.Count; i++)
{
    _renerList[i].material.SetFloat(“_IBLIntencity”, _IBLIntencity);
}

Texture2D textureY = _unityARVideo.VideoTextureY;
Texture2D textureCbCr = _unityARVideo.VideoTextureCbCr;

_yuvMat.SetTexture(“_textureY”, textureY);
_yuvMat.SetTexture(“_textureCbCr”, textureCbCr);

Graphics.Blit(null, _ARTexture, _yuvMat);
_blur.ExecuteBlur(_ARTexture, _bluredTexture);

_camera.RenderToCubemap(_cubeMap);

UnityARVideoクラスからテクスチャを取り出して、それをひとまず専用のRenderTextureにレンダリングします。
そしてレンダリングされた結果を、ブラー用のマテリアルでレンダリングしたものを全天球のテクスチャにします。
(上のコードにはありませんが、セットアップの時点で_bluredTextureが適切に割り当てられています)

そして最後に、_camera.RenderToCubemap(_cubeMap);を実行して、ぼかしたテクスチャをまとった全天球をCubemapに書き出している、というわけです。

キャラのシェーダ

キャラのシェーダのコード断片を載せておきます。

v2f vert(appdata i)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(i.vertex);
    o.normal = UnityObjectToWorldNormal(i.normal);
    o.uv = i.texcoord;
    return o;
}

float4 frag(v2f i) : SV_Target
{
    float4 tex = tex2D(_MainTex, i.uv);
    float4 cube = texCUBE(_Cube, i.normal) * _IBLIntencity;
    return tex * cube;
}

やっていることはシンプルに、テクスチャの色と、生成したCubeMapからの色を合成しているだけですね。
いちおう、あとから調整できるようにIntencityも用意してあります。

ただあくまで今回のやつは疑似的なものです。
そもそも、カメラで撮影している前面の映像しかないですし、本来適用されたい色とはずれているため、色味を調整する、くらいの感じで利用するのがいいかなと思います。
とはいえ、環境光にまったく影響を受けないのはそれはそれで違和感があるので、少しでも変化があるとより自然になるのではないでしょうか。

その他メモ

さて、今回は以上なんですが、サンプルの実装をするにあたって、いくつか別のアプローチも試していたので、せっかくなのでメモとして残しておこうと思います。

Skyboxのマテリアルで直接レンダリング

前述したものは、オブジェクトとして全天球を用意してそれをカメラでCubemapに変換する方式でした。
こちらは、Skyboxのマテリアルとしてのシェーダを書いて実現しようとしたものです。

Unityでは、環境マップ用にSkyboxのマテリアルが設定できるようになっています。
そのマテリアルには、他のマテリアルとは若干異なる値が渡されます。

これを用いて、Skyboxのレンダリング結果自体を操作することで実現しようとしたものです。

具体的には、前述の例と同じくUnityARVideoからYCbCrの2種類のテクスチャを取得するところまでは同様です。
それを直接、Skyboxのマテリアルにセットし、シェーダ内ではUV座標を極座標に変換してダイレクトに、フェッチする位置を計算する、というものです。

極座標への変換については以前記事に書いたので参考にしてみてください。

qiita.com

まずはそのシェーダを下に書きます。

Skyboxシェーダ

Shader "Skybox/ARSkybox"
{
    Properties
    {
        _TextureY("TextureY", 2D) = "white" {}
        _TextureCbCr("TextureCbCr", 2D) = "black" {}
    }

   CGINCLUDE

   #include "UnityCG.cginc"

   #define PI 3.141592653589793

   struct appdata
    {
        float4 position : POSITION;
        float3 texcoord : TEXCOORD0;
    };
    
   struct v2f
    {
        float4 position : SV_POSITION;
        float3 texcoord : TEXCOORD0;
    };

    float4x4 _DisplayTransform;
    
   sampler2D _MainTex;
    sampler2D _TextureY;
    sampler2D _TextureCbCr;
    
   v2f vert (appdata v)
    {
        v2f o;
        o.position = UnityObjectToClipPos (v.position);
        o.texcoord = v.texcoord;
        return o;
    }
    
   half4 frag (v2f i) : COLOR
    {
        float u = atan2(i.texcoord.z, i.texcoord.x) / PI;
        float v = acos(i.texcoord.y) / PI;
        float2 uv = float2(v, u);

        //
        // 式を調べたら以下のものだったが、ARKitで使ってるのは少し違う?
        //
        // Y  =  0.299R + 0.587G + 0.114B
        // Cr =  0.500R - 0.419G - 0.081B
        // Cb = -0.169R - 0.332G + 0.500B
        //
        // | Y  | = |  0.299,  0.587,  0.114 |   | R |
        // | Cr | = |  0.500, -0.419, -0.081 | x | G |
        // | Cb | = | -0.169, -0.332,  0.500 |   | B |
        //
        // 逆行列をかけて求める。
        //
        // R = Y + 1.402Cr
        // G = Y - 0.714Cr - 0.344Cb
        // B = Y + 1.772Cb

        //
        // 計算結果が異なったが、いちおう残しておく
        //
        // float y = tex2D(_TextureY, uv).r;
        // float2 cbcr = tex2D(_TextureCbCr, uv).rg;
        // float r = y + 1.402 * cbcr.g;
        // float g = y - 0.714 * cbcr.g - 0.344 * cbcr.r;
        // float b = y + 1.772 * cbcr.r;
        // 
        // return float4(r, g, b, 1.0);

        float y = tex2D(_TextureY, uv).r;
        float4 ycbcr = float4(y, tex2D(_TextureCbCr, uv).rg, 1.0);

        const float4x4 ycbcrToRGBTransform = float4x4(
            float4(1.0, +0.0000, +1.4020, -0.7010),
            float4(1.0, -0.3441, -0.7141, +0.5291),
            float4(1.0, +1.7720, +0.0000, -0.8860),
            float4(0.0, +0.0000, +0.0000, +1.0000)
        );

        return mul(ycbcrToRGBTransform, ycbcr);
    }

   ENDCG

   SubShader
    {
        Tags { "RenderType"="Background" "Queue"="Background" }
        Pass
        {
            ZWrite Off
            Cull Off
            Fog { Mode Off }

           CGPROGRAM
            #pragma fragmentoption ARB_precision_hint_fastest
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    } 
}

やっていることは、ARCameraから得たふたつのテクスチャ(※)を合成して、さらに全天球の位置を想定して、フェッチするUV座標を計算しています。
※ YCbCrフォーマットなので、YテクスチャとCbCrテクスチャの2枚を合成する必要があります。

Skybox用シェーダに渡されるtexcoord

通常、シェーダに渡されるtexcoordは対象のモデルのUVの値が使われます。
しかしSkyboxの場合はそもそもモデルデータではなく仮想のもののため、通常のtexcoordの値とは異なった値が渡ってきます。
(ちなみにSkyboxのtexcoordfloat3型)

ではどんな値が渡ってくるのかというと、float3型の値で、ワールド空間でのXYZ方向が渡ってきます。
例えばZ軸方向に向いているベクトルは(0, 0, 1)、真上方向は(0, 1, 0)、といった具合に、ワールド空間での、原点からの方向ベクトルがそのまま渡ってきます。

なのでそれを想定して、以下のよう、極座標に位置するテクセルをフェッチするようなイメージでフェッチする位置を変換します。
※ 以下のコードは実際に使っているものではなく、通常のテクスチャからフェッチする場合の計算です。

float u = 1 - atan2(i.texcoord.z, i.texcoord.x) / PI;
float v = 1 - acos(i.texcoord.y) / PI;
float2 uv = float2(u, v);

やっていることはまず、V座標についてはY軸方向をフェッチするため、単純にYの値からアークコサインで角度を求め、それを$\pi$、つまり180°で割ることで正規化しています。
さらに上下を逆転させるため、その値を1から引き、最終的な位置を決定しています。

続いてU座標については、XZ平面でのベクトルの角度を求め、それを$\pi$で正規化することで得ています。
※ ちなみに、U座標については本来は360°の角度がありますが、「見ている方向」に限定すると180°がちょうどいいので、あえて180°で正規化し、正面と背面で同じテクスチャを利用するようにしています。

実際には、ARCameraから得られる結果が若干回転した画像になっていたため、以下のように調整しました。

float u = acos(i.texcoord.y) / PI;
float v = atan2(i.texcoord.z, i.texcoord.x) / PI;

※ uとvの計算が逆になっていることに注意。

以上のように設定することで、下の図のように映像が全天球状態で表示されるようになります。

f:id:edo_m18:20171202215058p:plain

ハマったメモ

今回の例では(最終的には)問題なくなったんですが、ちょっとハマったのと知っておくといいかなと思った点をメモとして残しておきます。

ずばり、Cubemapを動的に反映させる方法、です。
最初、普通にRenderToCubemapを使ってCubemapにレンダリングしていたんですが、どうも最初の一回しか更新してくれない。(毎フレーム更新処理しているのに)

なんでかなーと色々調べていたところ、いくつかのパラメータ設定と更新の通知処理をしないとならないようでした。
そのときに実際に書いたコードを載せておきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CubeMapGenerator : MonoBehaviour
{
    [SerializeField]
    private Camera _otherCamera;

    [SerializeField]
    private Cubemap _cubemap;

    [SerializeField]
    private Material _material;

    private void Start()
    {
        Debug.Log(RenderSettings.defaultReflectionMode);
        RenderSettings.defaultReflectionMode = UnityEngine.Rendering.DefaultReflectionMode.Custom;
    }

    private void LateUpdate()
    {
        if (Input.GetKeyDown(KeyCode.A))
        {
            _otherCamera.RenderToCubemap(_cubemap);
            _cubemap.Apply();
            DynamicGI.UpdateEnvironment();
            RenderSettings.customReflection = _cubemap;
        }

        _otherCamera.transform.Rotate(Vector3.up, 1f);
    }
}

こんな感じで、Cubemapを更新してやらないと2回目以降のものが反映されませんでした。

UnityでThreadを使って処理を分割する

概要

Unityではスレッドを使うことが想定されていません。
というのも、いわゆる「Unity API」と呼ばれる様々なUnityの機能が、メインスレッド以外からは呼び出せない仕様となっているからです。
UIはメインスレッドからのみ操作できるというのと似ていますね。

とはいえ、昨今のゲームでは負荷の高い処理を行う必要があることも少なくありません。
そこで、Unity上でもスレッドを扱う必要が出てきます。

ということで、今回はUnityでスレッドを使う上での注意や実際に使う場合の処理などを書きたいと思います。

今回の記事を書くにあたって、処理負荷軽減の恩恵を感じられるように、Flocking、いわゆる群衆シミュレーションに似た処理をスレッドによって軽減するようにしてみました。
(ただ正直、スレッドの扱いにはそこまで慣れてないのでなんか変なところあったらツッコミ入れてください;)

なお、今回のデモはGithubにアップしてあります。

Flocking

フロッキングとは、いわゆる群衆シミュレーションと呼ばれる、生物が集団で移動する際の状況を「それっぽく」見せるためのアルゴリズムです。
実装自体はとてもシンプルで、いくつかのシンプルな実装を組み合わせるだけで、まるで鳥が集団で飛んでいるかのような状況を作り出すことができます。(Birdroidを短縮してBoid、と呼ばれるのも同じものです)

今回はこのアルゴリズムのうち、いくつかを組み合わせて、リーダー機に従い、それぞれの僚機が一定距離を保って飛行する、という感じのものを作ってみました。

↓こんな感じ。機体の追加と、ターゲットにまとわりつく、みたいな処理のつもりw

こちらの記事(【ゲームAI】フロッキングアルゴリズム)がAIとしてのフロッキングについて解説しているので興味がある人は読んでみてください。

スレッドを使う

今回のサンプルを実装する上で使用したスレッド関連のクラスは以下です。

  • ManualResetEvent
  • Thread

今回の例はシンプルなもののため、スレッドプールなどは使っていません。
また、Unity2017からはC#5.0以降で使えるawaitasyncが使えるようになります。
そのため、Taskなども使えるようになりますが、今回はスレッド自体の説明のためそれらは使用していません。

www.buildinsider.net

qiita.com

(今回のサンプルでは)ManualResetEventクラスを用いて、シグナルを切り替えながら同期処理を行います。
イメージは「信号機」です。セマフォも似た仕組みですね。

ManualResetEventを使い、Resetメソッドで「非シグナル状態」にします。
そしてその後、WaitOneメソッドを実行すると、スレッドはそこで待機状態となり、次にシグナルがオンになるまで停止されます。
シグナルがオンになったら(つまり信号が青になったら)スレッドが再開され、停止していた位置から処理を再開します。

各寮機の位置を更新するクラス

public class UnitWorker
{
    // 非シグナル状態で初期化
    private readonly ManualResetEvent _mre = new ManualResetEvent(false);

    private Thread _thread;
    private bool _isRunning = false;

    private float _timeStep = 0;

    public List<UnitBase> Units { get; set; }

    // コンストラクタ
    public UnitWorker()
    {
        Initialize();
    }

    // 初期化処理
    // スレッドを生成し、スタートさせておく
    private void Initialize()
    {
        _thread = new Thread(ThreadRun);
        _thread.IsBackground = true;
        _thread.Start();
    }

    // スレッドの再開を外部から伝える
    public void Run()
    {
        _timeStep = Time.deltaTime;
        _isRunning = true;
        _mre.Set();
    }

    // 実際の位置計算処理を実行
    private void Calculate()
    {
        UnitBase unit;
        for (int i = 0; i < Units.Count; i++)
        {
            unit = Units[i];
            unit.UpdatePosition(_timeStep);
        }
    }

    // サブスレッドで実行される処理
    private void ThreadRun()
    {
        // シグナル状態になるのを待機する
        _mre.WaitOne();

        try
        {
            // 位置計算のアップデート
            Calculate();
        }
        finally
        {
            // 最後に、非シグナル状態に戻して次回の実行が待機されるようにする
            _isRunning = false;

            _mre.Reset();

            // 新しいスレッドを作ってスタートさせておく(初期化と同じ)
            _thread = new Thread(ThreadRun);
            _thread.IsBackground = true;
            _thread.Start();
        }
    }
}

ユニットを生成・管理するクラス

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using UnityEngine;

public class DroneFactory : MonoBehaviour
{
    #region ### Variables ###
    [SerializeField]
    private Transform _leader;

    [SerializeField]
    private Transform _target;

    [SerializeField]
    private GameObject _unitPrefab;

    [SerializeField]
    private SteamVR_TrackedController _controller;

    private List<UnitBase> _units = new List<UnitBase>();
    public List<UnitBase> Units
    {
        get { return _units; }
    }

    private UnitWorker[] _unitWorkers = new UnitWorker[4];

    private bool _needsStopThread = false;
    #endregion ### Variables ###

    #region ### MonoBehaviour ###
    private void Start()
    {
        _units = new List<UnitBase>(GetComponentsInChildren<UnitBase>());

        for (int i = 0; i < _unitWorkers.Length; i++)
        {
            _unitWorkers[i] = new UnitWorker();
        }

        GiveUnits();
    }

    private void Update()
    {
        if (Time.frameCount % 5 == 0)
        {
            if (_controller.triggerPressed)
            {
                Injetion();
            }

            if (_controller.menuPressed)
            {
                GenerateUnit();
            }
        }

        for (int i = 0; i < _unitWorkers.Length; i++)
        {
            _unitWorkers[i].Run();
        }
    }
    #endregion ### MonoBehaviour ###

    /// <summary>
    /// 生成した4スレッド分に、計算するユニットを分配する
    /// </summary>
    private void GiveUnits()
    {
        int len = _unitWorkers.Length;
        int range = _units.Count / len;
        for (int i = 0; i < _unitWorkers.Length; i++)
        {
            List<UnitBase> units = _units.GetRange(range * i, range);
            _unitWorkers[i].Units = units;
        }
    }

    /// <summary>
    /// ユニットをターゲットに向けて射出する
    /// </summary>
    private void Injetion()
    {
        UnitBase unit = Units.FirstOrDefault(u => u.Target != _target);
        if (unit != null)
        {
            unit.Target = _target;
        }
    }

    /// <summary>
    /// ユニットを生成してリストに追加する
    /// </summary>
    private void GenerateUnit()
    {
        GameObject unitObj = Instantiate(_unitPrefab, _controller.transform.position, Quaternion.identity);
        UnitBase unit = unitObj.GetComponent<UnitBase>();
        unit.Leader = _leader;
        unit.Speed = Random.Range(0.2f, 0.5f);
        _units.Add(unit);

        GiveUnits();
    }
}

以上が、今回のサンプルの肝部分です。

解説

今回のサンプルは、ManualResetEventを使ってシグナル状態を管理、適切なタイミングでスレッドを起動し、位置を計算、計算後にそれを適用する、という流れになっています。
ポイントはスレッドの生成部分です。

実際はスレッドプールなどを生成して再利用しないと、毎フレームごとにスレッドを生成しているのでコストが高いですが、今回は分かりやすさ重視ということでこういう実装をしています。
スレッドを理解するには、スレッドは、OSからスケジューリングされて、決められた時間だけCPUを使い、計算を行う、という点です。

そのため、今回の_mre.WaitOne()のように、スレッド自体を停止させると、シグナル状態になるまでその処理が停止します。
メインスレッドで常に実行されるStartUpdateは、こうした「停止」処理自体が行なえません。

※ 厳密には、メインスレッドを停止してしまうと画面が固まって見えてしまうので、原則としてメインスレッドを待機状態にすることはまずないでしょう。
結局のところ、メインスレッドも「スレッドのひとつ」であることに変わりはないので、スレッドに対して行える処理は同様に行うことができます。

ざっくりと、理解の助けとなる手順を書くと以下のようになります。

  1. メソッド(ThreadRun)を、生成したスレッドに割り当ててそれを実行状態にする(Thread.Start
  2. ThreadRunメソッドは実行されてすぐに、_mre.WaitOne()によってシグナルを待つ状態に移行する
  3. Runメソッドが実行されると_mre.Set()が呼ばれ、シグナル状態となり、停止していたスレッドが動き出す
  4. スレッド(ThreadRun)の実行は、位置計算の更新処理後、最後のタイミングで再び非シグナル状態に戻し、さらに新しくスレッドを生成して終了する
  5. そして再び_mre.WaitOne()によってスレッドが停止され、以後それを繰り返す

という流れになります。

今回のサンプルではこの、「シグナル状態」が分かれば特にむずかしいことはないと思います。

参考記事

気になるとワスレルナ スレッドプログラミング AutoResetEvent

smdn.jp

スレッドプールの仕組みを作る

さて最後に、少しだけThreadPoolの仕組みを簡単に自作したものを載せておきます。
(ただ前述した通り、スレッドの扱いがまだ慣れてないので、あくまで自分の理解のために書いた感じなので注意してください)

参考: C#非同期処理関連のMSDNの資料読んでみた(2)

使うクラス

  • Thread
  • AutoResetEvent
  • WaitCallback

AutoResetEvent

前述のサンプルでも登場したManualResetEventですが、AutoResetEventというものもあります。 違いは以下です。

イベント待機ハンドル(WaitHandle)により、スレッドは相互に通知を行い、相手の通知を待機して動作を同期することができます。
イベント待機ハンドルは通知されたときに、自動的にリセットされるイベントと手動でリセットするイベントと2種類に分けられます。

ManualとAutoの違いはまさにこの「自動リセット」か「手動リセット」かの違いとなります。

AutoResetEventは待機中のスレッドがなくなると自動的に非シグナル状態へと遷移します。
一方、ManualResetEventは、Reset()を呼び出し、手動で非シグナル状態に戻す必要があります。

以下の記事が、ManualとAutoの違いの比較コードを載せてくれているので、興味がある人は読んでみてください。
参考: https://codezine.jp/article/detail/139#waithandle

※ それぞれのクラスはWaitHandleクラスを継承した派生クラスとなっています。

WaitHandle

Win32同期ハンドルをカプセル化し、複数の待機操作を実行するための抽象クラス。
派生クラスには上記以外に、Mutex, EventWaitHandle, Semaphoreなどがあります。

「待機ハンドル」と呼ばれるWaitHandleオブジェクトは、スレッドの同期に使われます。
待機ハンドルの状態には「シグナル状態」と「非シグナル状態」の2つがあり、待機ハンドルをどのスレッドも所有していなければ「シグナル状態」、所有していれば「非シグナル状態」となります。
WaitHandle.WaitOneメソッドなどを使うことにより、待機ハンドルがシグナル状態になるまでスレッドをブロックすることができます。
イメージ的には、「シグナル状態」は「青信号」で「非シグナル状態」は「赤信号」です。
つまり、非シグナル状態=赤信号の場合は、シグナル状態=青信号になるまで待機する、というわけですね。

WaitCallback

Define:
[ComVisibleAttribute(true)]
public delegate void WaitCallback(object state);

state ... コールバックメソッドが使用する情報を格納したオブジェクト。void*型と思えばよさげ。

コードサンプル

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public class SimpleThreadPool : MonoBehaviour
{
    /// <summary>
    /// サブスレッドタスク
    /// </summary>
    class Task
    {
        public WaitCallback Callback;
        public object Args;
    }

    private Queue<Task> _taskQueue = new Queue<Task>();
    private Thread _thread;
    private AutoResetEvent _are = new AutoResetEvent(false);
    private bool _isRunning = false;

    private int _id = 0;

    private void Start()
    {
        _isRunning = true;
        _thread = new Thread(new ThreadStart(ThreadProc));
        _thread.Start();
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.T))
        {
            AddTask();
        }

        if (Input.GetKeyDown(KeyCode.A))
        {
            for (int i = 0; i < 30; i++)
            {
                AddTask();
            }
        }
    }

    private void AddTask()
    {
        Task task = new Task
        {
            Callback = new WaitCallback(TaskProc),
            Args = (object)_id++,
        };

        _taskQueue.Enqueue(task);

        Debug.Log("Added task. Task count is " + _taskQueue.Count);

        if (_taskQueue.Count == 1)
        {
            _are.Set();
        }
    }

    private void TaskProc(object args)
    {
        Debug.Log("Task Proc.");

        Thread.Sleep(500);

        int id = (int)args;
        Debug.LogFormat("Task {0} is finished.", id);

        _are.Set();
    }

    private void ThreadProc()
    {
        while (_isRunning)
        {
            _are.WaitOne();

            if (_taskQueue.Count > 0)
            {
                Task task = _taskQueue.Dequeue();
                task.Callback(task.Args);
            }
        }
    }
}

こちらのサンプルでは、常にタスクを監視して実行するThreadProcをサブスレッドで実行し、タスクキューにタスクを追加することでスレッド処理を行っているサンプルです。
タスクが追加されるまではスレッドは停止状態になりますが、タスクが追加されるとスレッドが起動されて、キューからタスクを取り出し実行します。

今回はサンプルのため、タスク処理の中でシグナル状態を制御していますが、汎用的にタスクを追加することを考えるとここは内部で適切に管理する必要があるでしょう。

WaitCallbackでタスクを登録する

タスクの処理はTaskProcで行っていますが、タスク自体はWaitCallbackクラスに、処理してもらいたいメソッドを登録して生成しています。
定義はdelegateになっていて、object型の引数をひとつ受け取るデリゲートです。

なので、void*型のように使用して、内部で適切にキャストしてあげる必要があります。

このように、スレッドを必要数起動させておいて、タスクをあとからキューに追加する形で実行するので、スレッドの新規生成を挟まず、生成コストを削減することができるようになります。

その他

以前、C言語のスレッドについて、書籍からのメモを書いた記事もあるので、そちらも合わせて読んでみてください。

qiita.com

iOSのARKitを使ってVRのポジトラをやってみた

概要

ずっと気になっていたARKit。やっと触ることができたので、ひとまず、空間認識して色々触ったあと、VRのポジトラに流用するのをやってみたのでまとめておきます。
AR自体がポジトラしてモデルなんかを表示できるので、これをVRモードのカメラの位置に転化してあげる、という流れです。

実際に動かしてみた動画です↓

ひとまず、単純に空間を認識して平面などを配置、カメラを移動する、いわゆる「AR」の実装方法を解説したあと、VRモードへの転用を説明します。
(まぁといっても、ほぼAR空間での処理が実装できれば、あとはカメラの位置同期を別のものに置き換えるだけなので大した問題ではありませんが)

単純にARコンテンツをさっと作るだけなら、そもそもUnityが専用のコンポーネントをすでに用意してくれているので、それを組み合わせるだけですぐにでも空間にモデルなんかを配置することができます。
今回の解説は、VRに転用するにあたって、動作の仕組みなんかを把握したかったので、基本クラスを使いつつ、自前で実装するにはどうするか、という視点でのまとめです。
(とはいえ、ネイティブのARKitとの通信はほとんどUnity側でやってくれてしまうので、あまりむずかしいことはやりませんが)

準備

アセットのインポート

UnityのアセットストアですでにUnity ARKit Pluginが公式に配布されているのでそれをダウンロード、インポートします。

Bitbucketから最新のものが取得できるようなので、新しい機能なりを試したい場合は見てみるといいかも。

インポートが終わったら準備完了です。
ネイティブのARKitとの通信はすべてUnityのコンポーネントが行ってくれるので、それを使って構築していきます。

既存のコンポーネントを利用する

今回は、ネイティブからのデータを使って処理を行うように実装していますが、ARの機能をさっと試したいだけであれば、既存のコンポーネントを組み合わせるだけですぐにARの機能を使うことができるようになっています。

使うコンポーネント

使うコンポーネントUnityEngine.XR.iOS名前空間に定義されています。
余談ですが、Android向けビルドでVR SDKをDaydreamにすると、Unity2017以降だとXRという名前になっていて、ARCoreのチェックボックなんかも出てくるので、今後はVR x ARのポジトラは標準になりそうな予感がしますね。

使うコンポーネントは以下になります。

  • UnityARVideo
  • UnityARCameraNearFar
  • UnityARCameraManager
  • UnityARHitTestExample
  • UnityARSessionNativeInterface

UnityARVideo

iOSバイスのカメラの映像を、CommandBuffer経由で描画するようです。
カメラ自体にAddComponentして使います。
また、インスペクタにはマテリアルを設定するようになっていますが、ARKitアセットに含まれているYUVMaterialを設定してあげればOKです。

なお、カメラの映像を出力しないVRモードであっても、このコンポーネントがないとARカメラの位置トラッキングがおかしくなっていたので、もしかしたらARとしての画像解析がこのクラス経由で行われているのかもしれません。

UnityARCameraNearFar

ARカメラのNearとFarを適切に設定するコンポーネント・・・のようですが、これがないとなにがダメなのかはちょっとまだ分かっていません( ;´Д`)
このコンポーネントも、メインのカメラにAddComponentして使います。

UnityARCameraManager

カメラの動きを制御するコンポーネント。マネージャという名前の通り、これは、カメラにAddComponentするのではなく、空オブジェクトなどに設定して、インスペクタからカメラオブジェクトを登録する形で使います。

内部的な処理としては、後述するUnityARSessionNativeInterfaceクラスから、ARKitの解析データを受け取り、適切にカメラの位置をARで認識した空間に基いて移動する仕組みを提供します。

UnityARHitTestExample

画面をタッチした際に、タッチ先に平面が認識されていたらそこに3Dモデルなどを移動してくれるサンプル用コンポーネント
Hit testのやり方などが記述されているので、タッチに反応するアプリを作る場合などは参考にするとよさそうです。

UnityARSessionNativeInterface

ネイティブのARKitからの情報を受け取る最重要クラス。
基本的に、上記のような機能を自前で実装する場合はこのクラスからの値を適切に使う必要があります。

以上のコンポーネントを連携させるだけで、ARKitの機能を使った簡単なモデル配置などはすぐに行うことができます。

このコンポーネント群については、以下の記事を参考にさせていただきました。

qiita.com

ARKitの機能を使う

さて、上記のコンポーネントを使うことで簡単なモックならすぐ作れてしまうでしょう。
ここからは、それらのコンポーネントが行ってくれている部分を少し紐解きながら、ARを使ったコンテンツを作る上で必要になりそうな部分を個別に解説していきたいと思います。

ARKitで認識した位置をUnityのカメラと同期する

ARコンテンツをAR足らしめているのが、この「カメラの移動」でしょう。
空間を認識し、それに基いてカメラが適切に動いてくれることで、3Dオブジェクトなどが本当にそこにあるかのように見せることができるわけです。

UnityARCameraManagerを参考に、必要な部分だけ抜き出す

さて、先ほども紹介したUnityARCameraManagerには、このカメラの位置を同期する処理が書かれています。
といっても、内部的な処理はほぼUnityARSessionNativeInterfaceがやってくれるので、毎フレーム、現在の姿勢をカメラに適用するだけでOKです。

private void Start()
{
    _session = UnityARSessionNativeInterface.GetARSessionNativeInterface();

    ARKitWorldTrackingSessionConfiguration config = new ARKitWorldTrackingSessionConfiguration();
    config.planeDetection = UnityARPlaneDetection.Horizontal; // 現状は`None`か`Horizontal`しか選べない
    config.alignment = UnityARAlignment.UnityARAlignmentGravity;
    config.getPointCloudData = true;
    config.enableLightEstimation = true;
    _session.RunWithConfig(config);
}

private void Update()
{
    Matrix4x4 matrix = _session.GetCameraPose();
    _arCamera.transform.localPosition = UnityARMatrixOps.GetPosition(matrix);
    _arCamera.transform.localRotation = UnityARMatrixOps.GetRotation(matrix);
    _arCamera.projectionMatrix = _session.GetCameraProjection();
}

大雑把に説明すると、ARKitの動作のConfigを作成し、それを元にセッションを開始、以後はそのセッションから得られるカメラの位置や回転を、そのままカメラのlocalPositionlocalRotationに適用してやるだけです。
こうすることで、ARのカメラとして適切に移動、回転が行われます。

UnityARAnchorManagerを元に、平面の位置のトラッキングを行う部分を抜き出す

オブジェクトを配置して、カメラの移動が行われれば、基本的にはARらしい見た目を表現することは可能です。
次は、ARKitのシステムが認識した平面の情報を使って、実際に空間に平面情報を表示する方法を見てみます。

private Dictionary<string, ARPlaneAnchorGameObject> planeAnchorMap;


private void Start()
{
    planeAnchorMap = new Dictionary<string,ARPlaneAnchorGameObject> ();

    UnityARSessionNativeInterface.ARAnchorAddedEvent += AddAnchor;
    UnityARSessionNativeInterface.ARAnchorUpdatedEvent += UpdateAnchor;
    UnityARSessionNativeInterface.ARAnchorRemovedEvent += RemoveAnchor;
}

上記の3つのイベントが、ARKitのシステムから発行されます。
それぞれ、アンカー(平面)が認識された、更新された、破棄されたタイミングで呼ばれます。

そのイベント内でどんな処理が書かれているのか見てみましょう。

ARAnchorAddedEvent

まずは、平面が認識された際のハンドラ内での処理です。

public void AddAnchor(ARPlaneAnchor arPlaneAnchor)
{
    GameObject go = UnityARUtility.CreatePlaneInScene (arPlaneAnchor);
    go.AddComponent<DontDestroyOnLoad> ();  //this is so these GOs persist across scene loads
    ARPlaneAnchorGameObject arpag = new ARPlaneAnchorGameObject ();
    arpag.planeAnchor = arPlaneAnchor;
    arpag.gameObject = go;
    planeAnchorMap.Add (arPlaneAnchor.identifier, arpag);
}

平面が認識された際の処理は、まず、UnityARUtilityクラスのユーティリティを使って平面オブジェクトを生成します。
そして、ARPlaneAnchorGameObjectクラスのインスタンスを生成し、それぞれ、GameObjectARPlaneAnchorへの参照をセットにして保持します。

あとはそれを、マネージャクラス自身が持っているDictionaryに登録しておきます。
これを登録する理由は、平面の情報は、認識後に連続した状態を持つため(※)更新時に、identifierを元に処理を行う必要があるためです。

※ 連続した情報というのは、どうやらARKitが認識した平面は一意なIDが振られ、その平面の状態がどうなったか、という連続的な計算になるようです。
そのため、検知時のIDをキーにして登録し、更新があった場合に、それを元に平面の位置などを変更してやる必要があるのです。

ARAnchorUpdatedEvent

次に更新処理。
上記でも書きましたが、更新処理は、「平面の連続性」故に、検知時のIDを利用して更新処理を行います。

public void UpdateAnchor(ARPlaneAnchor arPlaneAnchor)
{
    if (planeAnchorMap.ContainsKey (arPlaneAnchor.identifier)) {
        ARPlaneAnchorGameObject arpag = planeAnchorMap [arPlaneAnchor.identifier];
        UnityARUtility.UpdatePlaneWithAnchorTransform (arpag.gameObject, arPlaneAnchor);
        arpag.planeAnchor = arPlaneAnchor;
        planeAnchorMap [arPlaneAnchor.identifier] = arpag;
    }
}

Dictionaryに登録のある平面だった場合に、その状態を更新する処理となります。

ARAnchorRemovedEvent

最後に、平面が破棄されたときの処理。
こちらはたんに、Dictionary内にあったらその情報を削除しているだけですね。

public void RemoveAnchor(ARPlaneAnchor arPlaneAnchor)
{
    if (planeAnchorMap.ContainsKey (arPlaneAnchor.identifier)) {
        ARPlaneAnchorGameObject arpag = planeAnchorMap [arPlaneAnchor.identifier];
        GameObject.Destroy (arpag.gameObject);
        planeAnchorMap.Remove (arPlaneAnchor.identifier);
    }
}

平面に対する処理は以上です。
あとは、ARKit側で検知、更新、破棄が起こるタイミングで平面情報が更新されていきます。

画面をタップした際に、その位置の平面にオブジェクトを移動させる

ARでモデルを表示するだけでもだいぶ楽しい体験ができますが、やはりタップしたりしてインタラクティブなことができるとより楽しくなります。

private void Update()
{
    // 中略。Touchの確認処理
    
    var screenPosition = Camera.main.ScreenToViewportPoint(touch.position);
    ARPoint point = new ARPoint {
        x = screenPosition.x,
        y = screenPosition.y
    };

    // prioritize reults types
    ARHitTestResultType[] resultTypes = {
        ARHitTestResultType.ARHitTestResultTypeExistingPlaneUsingExtent, 
        // if you want to use infinite planes use this:
        //ARHitTestResultType.ARHitTestResultTypeExistingPlane,
        ARHitTestResultType.ARHitTestResultTypeHorizontalPlane, 
        ARHitTestResultType.ARHitTestResultTypeFeaturePoint
    }; 

    foreach (ARHitTestResultType resultType in resultTypes)
    {
        if (HitTestWithResultType (point, resultType))
        {
            return;
        }
    }
}

まずは、Updateメソッド内での処理です。
基本的なタッチ判定処理後だけを抜き出しています。

画面のタッチされた位置をViewport座標に変換したのち、ARpointクラスに値を設定します。
そして、検知したいResultTypeの配列を作り、順次、そのタイプに応じてタッチ位置との判定を行います。
判定は同クラスに設定されたHitTestWithResultTypeで行います。

bool HitTestWithResultType (ARPoint point, ARHitTestResultType resultTypes)
{
    List<ARHitTestResult> hitResults = UnityARSessionNativeInterface.GetARSessionNativeInterface ().HitTest (point, resultTypes);
    if (hitResults.Count > 0) {
        foreach (var hitResult in hitResults) {
            Debug.Log ("Got hit!");
            m_HitTransform.position = UnityARMatrixOps.GetPosition (hitResult.worldTransform);
            m_HitTransform.rotation = UnityARMatrixOps.GetRotation (hitResult.worldTransform);
            Debug.Log (string.Format ("x:{0:0.######} y:{1:0.######} z:{2:0.######}", m_HitTransform.position.x, m_HitTransform.position.y, m_HitTransform.position.z));
            return true;
        }
    }
    return false;
}

UnityARSessionNativeInterfaceHitTestメソッドが定義されているので、それを用いてヒットテストを行っています。
もしヒットした平面があった場合はヒット結果が1以上にあるため、それを元に分岐処理を行い、ヒットしたらその情報を出力しています。

なお、このクラスではタッチ位置にオブジェクトを移動する処理が含まれているので、同時に、設定されたオブジェクトの位置を変更する記述が見られます。

ARカメラを使ってポジトラする

さて最後に。
今回のAR関連の機能を使って、VRでのポジトラをする方法を説明します。
といっても、今までの処理を少し変えるだけなので、実装自体は大したことはしません。

まずはざっとコードを見てもらったほうが早いでしょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.iOS;

public class ARAnchorUpdater : MonoBehaviour
{
    [SerializeField]
    private Transform _target;

    [SerializeField]
    private Camera _arCamera;

    [Header("---- AR Config Options ----")]
    [SerializeField]
    private UnityARAlignment _startAlignment = UnityARAlignment.UnityARAlignmentGravity;

    [SerializeField]
    private UnityARPlaneDetection _planeDetection = UnityARPlaneDetection.Horizontal;

    [SerializeField]
    private bool _getPointCloud = true;

    [SerializeField]
    private bool _enableLightEstimation = true;

    private Dictionary<string, ARPlaneAnchorGameObject> _planeAnchorMap = new Dictionary<string, ARPlaneAnchorGameObject>();

    private UnityARSessionNativeInterface _session;

    private void Start()
    {
        _session = UnityARSessionNativeInterface.GetARSessionNativeInterface();

        Application.targetFrameRate = 60;
        ARKitWorldTrackingSessionConfiguration config = new ARKitWorldTrackingSessionConfiguration();
        config.planeDetection = _planeDetection;
        config.alignment = _startAlignment;
        config.getPointCloudData = _getPointCloud;
        config.enableLightEstimation = _enableLightEstimation;
        _session.RunWithConfig(config);
    }

    private void Update()
    {
        // セッションからカメラの情報をもらう
        Matrix4x4 matrix = _session.GetCameraPose();

        _target.transform.localPosition = UnityARMatrixOps.GetPosition(matrix);

        // VRカメラでジャイロを使って回転するため、ここでは回転を適用しない
        //_target.transform.localRotation = UnityARMatrixOps.GetRotation(matrix);

        // ARカメラのプロジェクションマトリクスを更新
        // TODO: もしかしたらいらないかも?
        _arCamera.projectionMatrix = _session.GetCameraProjection();
    }
}

さて、見てもらうと分かりますが、上で書いたUnityARCameraManagerの中身を少しカスタマイズしただけですね。

違う点は、_targetに、ARカメラの移動を適用するためのオブエジェクトを、インスペクタから設定しているだけです。
Updateメソッド内を見てもらうと、カメラの位置同期の処理が_targetに対して行われているのが分かりますね。

そしてもうひとつ注意点として、「回転は適用しない」ということ。
なぜかというと、_targetに指定しているオブジェクトの子要素に、VRカメラが存在しているためです。
そしてVRカメラはCardboard SDKが、ジャイロを使って自動的に回転処理をしてくれます。

つまり、ARカメラの回転も伝えてしまうと、回転が二重にかかってしまうわけなんですね。
(最初それに気づかず、なんで180°回転しただけなのに一周しちゃうんだろうとプチハマりしてました・・)

なので位置だけを同期してあげればいいわけです。
まさに「ポジトラだけ」ARカメラからもらっている感じですね。

以上で、モバイルVRでもポジトラができるようになります。

その他、参考にした記事

lilea.net

recruit.gmo.jp