e.blog

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

UnityのネイティブプラグインをC/C++で作成する準備

概要

ネイティブで書かれたプラグインを、使うことはあっても自分で書いたことがなかったのでHello Worldしてみたメモです。

ちなみに、できるだけ動作を把握する意味も込めてコマンドラインで作ることを前提としています。

開発環境を整える

最初、Git Bashを使っていたのでそこにgccコマンドが入っていてそれを利用していたのですが、ごく簡単な関数を書いてインポートしたところエラーが。
(と思ったら、会社のPCでの環境だった。いつgcc入れたっけな・・。デフォルトでは入ってませんでした)

$ Failed to load 'Assets/Plugins/************.dll', expected 64 bit architecture 

64bitを期待してるけど、32bit向けに作られたものですよ、ということ。
ならばと、64bit向けにコンパイルすればいいのね、はいはい、と思いつつ、-m64オプションを付けて実行するも・・・

$ sorry, unimplemented: 64-bit mode not compiled in

MINGW64って書いてあるのに、なんでさ。

その後、色々調べてみたら、MINGW64自体を使うこと自体は間違っていない模様。
64bit版で書き出せるコマンドがあるらしく探してみるものの、どうもGit Bashはビルドが違うのか、該当のコマンドが見当たりませんでした。

なので、別途新しくMINGW64をインストール。
このへん(Windowsの無料で使える 64bit/32bit C/C++コンパイラ)を参考にしました。

x86_64向けにコンパイルするには、以下の位置にあるコマンドを利用します。

path/to/location/mingw-w64/x86_64-7.2.0-win32-seh-rt_v5-rev1/mingw64/bin/x86_64-w64-mingw32-gcc

コンパイルする

これに、-m64オプションを付けてコンパイルしたところ、無事に64bit向けにコンパイルすることができました。

$ x86_64-w64-mingw32-gcc -m64 -c anyplugin.c

そして、生成されたオブジェクトファイル(.o)を、DLLに変換します。

$  x86_64-w64-mingw32-gcc -shared -o anyplugin.dll anyplugin.o

これで無事にDLLファイルが作成されます。

※ Pathを通して↑のコマンドを使ってます。

コンパイルされたファイルのアーキテクチャを確認する

ちなみに、すでにコンパイルされているdllやオブジェクトファイルがどのアーキテクチャ向けにビルドされているかを確認するには、Visual Studioに同梱されているdumpbin.exeを利用すると分かるようです。

該当のexeは以下の場所らへんにあります。(インストール環境による)

C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin

このあたりの詳細は以下を参照ください。

rms-099.hatenablog.jp

d.hatena.ne.jp

そして、チェックしたいファイルを引数に以下のように実行します。

$ dumpbin /headers anyplugin.o

すると、ダーっと色々な情報が表示されますが、先頭のほうにある、

// ... 略 ...

FILE HEADER VALUES
            8664 machine (x64)

// ... 略 ...

を見てみると、x64と書かれているので、64bit向けにコンパイルできていることが確認できます。

Cで書いたDLLを読み込んでUnityで実行する

さて、これでDLLを作成する準備が整いました。
最後に、ごく簡単なサンプルを書いて終わりにしたいと思います。

Cで処理を書く

今回はサンプルなので、Cで簡単な処理を書いてみます。

// plugin.c
int Add(int a, int b)
{
    return a + b;
}

さて、これをUnityのC#側で利用できるようにします。
前述のようにコンパイルを行い、DLLファイルを作成します。

そしてそれを、Assets/Pluginsフォルダにコピーし、以下のようにC#側で呼び出します。

// ... 略 ...

using System.Runtime.InteropServices; // <- DllImportを使うために追加

public class AnyClass : MonoBehaviour
{
    private void Start()
    {
        int test = Add(1, 2);
        Debug.Log(test); // -> 3
    }


    // プラグインのファイル名を指定する
    [DllImport("plugin")
    private static extern int Add(int a, int b);
}

橋渡しができたら、あとは実際の処理を書いていくことでネイティブ側のコードを実行することができるようになります。

参考

なお、共有ライブラリのコンパイル周りについては、過去にQiita記事に書いているので、よかったら見てみてください。

qiita.com

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