e.blog

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

Unreal Engine C++ 逆引きメモ

目次

概要

まだまだUEに慣れていないので、色々なことを忘れる前に逆引きでメモしておきます。
(なので、随時更新予定)

また、UEでC++を書くにあたって理解しておかなければならない点として、標準のC++とは「異なる」という点です。
どういうことかと言うと、UEではガベージコレクション、通称GCと呼ばれる仕組みを導入しています。

しかし、当然ながらC++にはガベージコレクションはありません。
そこでUEでは、独自のインスンタンス生成の仕組みや、マクロを用いたプリプロセッサ経由で様々な、GCのための準備をしてくれます。

そのため、標準のC++とUEで使うC++、言ってみればUE C++とで作法が違う、という点を覚えておく必要があります。

生成・取得・削除

C++クラスの生成

通常のC++では、newを用いてインスタンスを生成します。
しかし、UE C++ではインスタンスの生成方法に違いがあります。理由は前述のように、GC対象として管理するためです。
最近ではスマートポインタを使ったりしますが、それと似た感じですね。

UMyClass MyClass = NewObject<UMyClass>();

// オーナーを指定する場合は引数に入れる
// UMyClass MyClass = NewObject<UMyClass>(Owner);

当然のことながら、UE管理下に置かないような純粋なC++で書く処理についてはこの作法は適用されません。

コンストラクタ内でNewObjectは使えない

どうやら、コンストラクタ内では上記のNewObjectは使えないようです。(使うとクラッシュする)

ではどうするかというと、FObjectInitializer::CreateDefaultSubobjectを利用します。
FObjectInitializerは、コンストラクタ引数に指定しておくと、UEシステムが適切に渡してくれるようになっています。(詳細は後述)

以下のようにコンストラクタを定義することで利用できるようになります。

UAnyClass::UAnyClass(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    // オブジェクト生成
    UAthorClass* instance = ObjectInitializer.CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

しかし実はこのCreateDefaultSubobjectは、UObject基底クラスでラッパーが実装されているようで、オブジェクトを生成する目的だけであれば、CreateDefaultSubobjectを直に呼ぶことで同様のことを達成することができます。

UAnyClass::UAnyClass()
{
    // オブジェクト生成
    UAthorClass* instance = CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

なおこのコンストラクタに引数を指定した場合としない場合の挙動の差ですが、UEシステムが自動的に生成する****.generated.h内にてマクロが生成され、コンストラクタの定義に応じて書き換わるよういなっているようです。

詳細はこちら([UE4] ObjectInitializerでコンポーネント生成を制御する | 株式会社ヒストリア)の記事をご覧ください。

PlayerController / PlayerPawnを取得する

ゲーム開始時に生成されたプレイヤーコントローラ / プレイヤーポーンを取得するには、Kismet/GameplayStatics.hを読み込む必要があります。

PlayerController

#include "Kismet/GameplayStatics.h"

APlayerController* PlayerController = UGameplayStatics::GetPlayerController(this, 0);
if (PlayerController)
{
    // do anything.
}

PlayerPawn

#include "Kismet/GameplayStatics.h"

// UGameplayStatics::GetPlayerPawnを介して取得し、適切にキャストする
AAnyCharacter* MyCharacter = Cast<AAnyCharacter>(UGameplayStatics::GetPlayerPawn(this, 0));
if (MyCharacter)
{
    // do anything.
}

GameModeとの関連

なお、このPlayerControllerPlayerPawnの関係は、PawnをコントロールするのがPlayerControllerの役割です。
これらの設定はGameModeに設定するようになっており、またGameModeはプロジェクト設定にて設定され、これが実行時に起動するポイントとなるようです。

詳細はこちら↓

msyasuda.hatenablog.com

ワールドに存在するアクターをすべて取得する

#include "Kismet/GameplayStatics.h"

// find all AnyActors
TArray<AActor*> FoundActors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AAnyActor::StaticClass(), FoundActors);

for (auto Actor : FoundActors)
{
    AAnyActor* AnyActor = Cast<AAnyActor>(Actor);
    if (AnyActor )
    {
        // do anything.
    }
}

アクターオブジェクトを生成する

オブジェクトの生成には、UWorldクラスのメソッドを利用します。

// FActorSpawnParametersを使うのに必要
#include "Runtime/Engine/Classes/Engine/World.h"

// AActor::GetWorldから、UWorldを得る
UWorld* const World = GetWorld();

// Nullチェック
if (!World)
{
    return;
}

FVector Location(0.0f, 0.0f, 0.0f);
FRotator Rotator(0.0f, 0.0f, 0.0f);
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = Instigator;

AAnyActor* AnyActor = World->SpawnActor<AAnyActor>(ActorBP, Location, Rotation, SpawnParams);

削除中かを知る

まだ理解が浅いですが、Unityと同様、Destroyを実行しても即座にメモリから消えるわけではなく、ゲームとして破綻しないよう様々な終了処理が存在するはずです。
そのため、削除中、という状態が存在します。
それをチェックするには以下のようにします。

AActor* anyActor = ...;
anyActor->IsPendingKill();

移動

アクターを移動させる

AActorクラスにはGetActorLocationSetActorLocationがあるのでこれを利用する。

FVector location = actor->GetActorLocation();
location.X += 10.0f;
actor->SetActorLocation(location);

ちなみに、SetActorLocationの定義を見ると以下のようになっています。

/** 
 * Move the actor instantly to the specified location. 
 * 
 * @param NewLocation  The new location to teleport the Actor to.
 * @param bSweep       Whether we sweep to the destination location, triggering overlaps along the way and stopping short of the target if blocked by something.
 *                     Only the root component is swept and checked for blocking collision, child components move without sweeping. If collision is off, this has no effect.
 * @param Teleport     How we teleport the physics state (if physics collision is enabled for this object).
 *                     If equal to ETeleportType::TeleportPhysics, physics velocity for this object is unchanged (so ragdoll parts are not affected by change in location).
 *                     If equal to ETeleportType::None, physics velocity is updated based on the change in position (affecting ragdoll parts).
 *                     If CCD is on and not teleporting, this will affect objects along the entire swept volume.
 * @param OutSweepHitResult The hit result from the move if swept.
 * @return Whether the location was successfully set if not swept, or whether movement occurred if swept.
 */
bool SetActorLocation(const FVector& NewLocation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr, ETeleportType Teleport = ETeleportType::None);

引数を色々変更することによって、移動後の物理干渉などに対する設定や演算結果を受け取ることができるようです。

カメラの向いている方向に移動させる

まず、Worldからカメラマネージャを取得し、そこからカメラの前方を取得、それを元にアクターを移動させる、という手順で行います。

#include "Engine.h" // GEngineを使うのでインクルードしておく

void AMyActor::BeginPlay()
{
    if (GEngine != nullptr)
    {
        // CameraManagerをワールドから取得する
        CameraManager = GEngine->GetFirstLocalPlayerController(GetWorld())->PlayerCameraManager;
    }
}

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    FRotator cameraRot = CameraManager->GetCameraRotation();
    FVector dir = cameraRot.Vector();
    dir.Normalize();

    FVector location = GetActorLocation();
    location += dir * 10.0f * DeltaTime;
    
    SetActorLocation(location);
}

コンポーネント

Blueprintに、カスタムコンポーネントを表示させる

UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))
class PROJECTNAME_API UHogeMovementComponent : public UPawnMovementComponent
{
    // ... 略 ...
}

こんな感じで、UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))を指定すると、BPのAddComponentのリストに表示されるようになります。

コンポーネントのセットアップ

コンポーネントを適切にセットアップしないと、TickComponentが呼ばれないなど問題が出るため、適切にセットアップする必要があります。

void USampleComponent::AttachSampleComponent(AActor* Actor)
{
    if (!Actor)
    {
        return;
    }

    // Componentを新規生成
    UOtherComponent* Comp = NewObject<UOtherComponent>(Actor);

    // アクタにアタッチする
    Actor->AddInstanceComponent(Comp);

    // UActorComponent::RegisterComponentで、イベントループに登録する
    Comp->RegisterComponent();
}

追加されているコンポーネントを取得する

TArray<AnyComponent*> components;
GetComponents<AnyComponent>(components);

// コンポーネントの数を確認
UE_LOG(LogTemp, Log, TEXT("Count: %d"), components.Num());

Ownerを取得する

ComponentはActorのコンポーネントとして振る舞うため、オーナーを取得して操作することが増えるかと思います。
オーナーの取得は以下のようにします。

AActor *owner = GetOwner();

ComponentからInputを使う

InputまわりはUInputComponentが司ります。
UActorComponentではUInputComponentを保持していないので、オーナーなどから取得して適切にセットアップする必要があります。

PawnクラスのサブクラスなどではSetupPlayerInputComponentのタイミングでUInputComponentが渡ってくるので、そこでセットアップの機会があるようです)

https://docs.unrealengine.com/latest/INT/Programming/Tutorials/PlayerCamera/3/docs.unrealengine.com

AActor *actor = GetOwner();
actor->InputComponent->BindAction("Fire", IE_Pressed, this, &UAnyComponent::FireHandler);

void UAnyComponent::FireHandler()
{
    UE_LOG(LogTemp, Log, TEXT("Fire!!!");
}

※ Input系については、UE4のお作法に則って適切にセットアップする必要があります。

セットアップについては、以下の引越しガイドの「入力イベント」あたりに載っています。

docs.unrealengine.com

Static Meshを生成する

USceneComponent *root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
UStaticMeshComponent *mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("mesh"));
mesh->SetupAttachment(root);

static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("[reference]"));
UStaticMesh *asset = MeshAsset.Object;

mesh->SetStaticMesh(asset);

ちなみに、[reference]の部分は、UEエディタのアセットのコンテキストメニュー内のCopy Referenceから得ることができます。

f:id:edo_m18:20180208102926p:plain

参考:
forums.unrealengine.com

Colliderを設定する

コライダの設定にはUSphereComponentなどを利用します。
詳細パネルではSphere Collisionとか表示されるやつです。

AMyPawn::AMyPawn()
{
    // ... 中略 ...

    // Sphereコライダを生成
    USphereComponent *sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
    sphere->InitSphereRadius(100.0f);
    sphere->SetSimulatePhysics(false);
    sphere->SetCollisionProfileName(TEXT("BlockAll"));

    // Sphereの見た目を生成
    UStaticMeshComponent *sphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SphereVisual"));
    sphereVisual->AttachTo(sphere);

    static ConstructorHelpers::FObjectFinder<UStaticMesh> meshAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    if (meshAsset.Succeeded())
    {
        sphereVisual->SetStaticMesh(meshAsset.Object);
        sphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
        sphereVisual->SetWorldScale3D(FVector(0.8f));
    }
}

セットアップ時に行っているのは3点。

  • sphere->InitSphereRadius(100.0f);
  • sphere->SetSimulatePhysics(false);
  • sphere->SetCollisionProfileName(TEXT("BlockAll"));

です。それぞれ上から、

  • 球体の半径の設定
  • 物理挙動させるか(falseの場合は、Unityで言うIsKinematic = trueの状態)
  • コリジョンの仕方

となります。
特に最後の「コリジョンの仕方」は、衝突するのか、イベントのみ(overlap)なのか、というところ設定するので、意図した通りに設定しておかないと「衝突しないじゃん」となるので注意です。(というか、ここを勘違いしていてずっと衝突しなくてちょっとハマった)

コリジョンを処理する

コリジョンを設定したあとは、コリジョン発生時になにかしらの処理をしたい場合があります。
その場合に使えるのが、ColliderのもつGetOverlappingActorsメソッドです。

コライダの範囲にあるAActorオブジェクトを取得してくれます。

※ 以下のコードは、公式のチュートリアルの中で使われているコードの抜粋です。

void ABatteryCollectorCharacter::CollectPickups()
{
    // Get all overlapping Actors and store them in an array
    TArray<AActor*> CollectedActors;
    CollectionSphere->GetOverlappingActors(CollectedActors);

    // For each Actor we collected
    for (int32 iCollected = 0; iCollected < CollectedActors.Num(); ++iCollected)
    {
        // Cast the actor to APickup
        APickup* const TestPickup = Cast<APickup>(CollectedActors[iCollected]);

        // If the cast is successful and the pickup is valid and active
        if (TestPickup && !TestPickup->IsPendingKill() && TestPickup->IsActive())
        {
            // call the pickup's WasCollected function
            TestPickup->WasCollected();

            // Deactivate the pickup 
            TestPickup->SetActive(false);
        }
    }
}

Blueprintで利用する

C++で書いたクラス(コンポーネント)も、ブループリントから利用するように作成することが出来ます。
そのためのマクロがUPROPERTYUFUNCTIONです。
これらを適切に設定することで、ブループリントから設定できたり、あるいはGetter / Setterとして機能したり、あるいはブループリントで実装を促す、なんてこともできるようになります。

以下に、キーワードと意味を、よく見るものを抜粋して記載しておきます。
※ 英語ドキュメントの翻訳(意訳)なので、詳細についてはドキュメントをご覧ください。

Keyword 意味
BlueprintImplementableEvent ブループリント(ノード)でオーバーライドするように促す。そのため、Body(実装)部分は書いてはならない。UEにより、(ブループリントで)オーバーライドされた本体を実行するProcessEventを呼び出すためのコードが自動生成される
BlueprintNativeEvent BlueprintImplementableEvent同様、ブループリントでオーバーライドするようデザインされたものであるが、違いとしてはC++による実装を行う点。実装本体は[FunctionName]_Implementationを実装する必要がある。元の[FunctionName]内には、自動生成コードとして、[FunctionName]_Implementationを呼び出すコードが追加される(※1)
BlueprintPure このキーワードをつけられた関数は、副作用を起こさないものとしてマーク付けられ、さらにBlueprintCallableを意味します。Getメソッドの実装に適したものです。さらに、non-pureになるようにconst functionでfalseとマークすることもできる(※2)
BlueprintCallable Blueprintから呼び出しできるようにマークする
Category カテゴリを定義する。設定すると、Blueprint上でカテゴライズされて表示される

※1 [FunctionName]\_Implementationを実装しないとコンパイルエラーになる。定義すると、BPのEvent Graphで配置して利用できるようになる。以下、サンプル↓

UFUNCTION(BlueprintNativeEvent)
void HogeHoge();

virtual void HogeHoge_Implementation();

f:id:edo_m18:20180225214321p:plain

※2 PureとNon-Pureについて書かれている記事があったので、詳細はこちらをご覧ください→ [UE4] Pure関数とNonPure関数|株式会社ヒストリア

ドキュメント:UFUNCTION - Unreal Engine Wiki

Blueprintからプロパティに値を設定できるようにする

コンポーネントを作成し配置しても、適切にマクロを設定しておかないとBPからプロパティに対して値を設定することができません。

以下のように設定することでそれが可能となります。

UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn)

ドキュメント: UPROPERTY - Epic Wiki


その他

enumを定義する

l参考にしていたVideo Tutorialは若干古いバージョンだったため、その中で説明されていたenumだとエラーが出てコンパイルができませんでした。
ということで、いちおうメモ。

UENUM(BlueprintType)
enum class EHogeEnum : uint8
{
    EFuga,
    EFoo,
    EBar,
};

と、uint8を継承した形で宣言しないとなりません。

タイマーを利用する

タイマーの利用には、FTimerManagerクラスを利用します。
以下の例では、自身を再帰的にタイマーで呼び出します。

// header
FTimeHandle Timer;
void AAnyActor::TimerHandler();

// implementation
void AAnyActor::TimerHandler()
{
    // &AActor::GetWorldTimerManager
    float Delay = FMath::FRandRange(1.0f, 2.0f);
    GetWorldTimerManager().SetTimer(Timer, this, &AAnyActor::TimerHandler, Delay, false);
}

ドキュメントはこちら

docs.unrealengine.com


トラブルシューティング

TickComponentが呼ばれなくなった!

C++を書いていて、突然、少し前まで正常に動いていたTickComponentが動かなくなるケースがありました。

色々調べてみても、必要なフラグの扱いやらメソッドの定義やらは正常に行っている・・でも動かない。

最終的に解決したのは、「該当のComponentを一度消し、追加し直す」ことで解消しました。
そのあたりが書かれていた記事がこちら↓

community.gamedev.tv

まさか入れ直しだけで解決するとは・・。
おそらくですが、(社内のエンジンに詳しい人と話していて聞いたのは)Hot Reloadの機能がUE4には備わっていて、それの関連付けなどがおかしくなってしまったのでは、とのこと。
多分、その関連付け周りの処理が、コンポーネントの追加・削除のタイミングで行われているのでしょう。

なので、追加し直しで直ったのではないかなと。

ちなみに、TickComponentを呼ぶ必要があるかどうか、みたいなフラグ周りについては以下の記事が色々まとめてくれているので参考にしてみてください。

usagi.hatenablog.jp

case内の初期化

これはUEというよりC++の問題ですが、switch文内で初期化を伴う処理を書いている場合、case文を{}で囲まないとコンパイルエラーとなるようです。


その他Tips

色々な値をログ出力

Log Fomatting

  • LogMessage
UE_LOG(LogTemp, Log, TEXT("Hoge"));
  • Log an FString
FString anyString = ...;
UE_LOG(LogTemp, Log, TEXT("Log: %s"), *anyString);
  • Log an Bool
bool anyBool = ...;
UE_LOG(LogTemp, Log, TEXT(Bool value: %s"), anyBool ? TEXT("True") : TEXT("False")); 
  • Log an Int
int anyInt = ...;
UE_LOG(LogTemp, Log, TEXT("Int value: %d"), anyInt);
  • Log a Float
float anyFloat = ...;
UE_LOG(LogTemp, Log, TEXT("Float value: %f"), anyFloat);
  • Log an FVector
FVector anyVector = ...;
UE_LOG(LogTemp, Log, TEXT("FVector value: %s"), *anyVector.ToString());
  • Log an FName
FName anyName = ...;
UE_LOG(LogTemp, Log, TEXT("FName value: %s"), *anyName.ToString());

ドキュメントはこちら → Logs, Printing Messages To Yourself During Runtime

Tickメソッド外で時間を扱う

Tickメソッド内では、引数にDeltaTimeが渡ってくるのでそれを利用すればいいですが、それ以外の場合はワールドから取得する必要があります。

void AnyMethod()
{
    float time = GetWorld()->GetTimeSeconds();
    float deltaTime = GetWorld()->GetDeltaSeconds();

    UE_LOG(LogTemp, Log, TEXT("Time: %f, DeltaTime: %f"), time, deltaTime);
}

番外編

ちょっと昔に書いた記事ですが、Cocos2D-xでアプリを作る際にまとめた、C++関連の記事です。
UEとは関係ない部分もありますが、C++的なところは同じなので紹介。

qiita.com

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回目以降のものが反映されませんでした。