作成者: rarihoma

[UE4] Unreal Engine Meetup Nagoya 6 in 名古屋城 で講演しました

02/25 に、Unreal Engine Meetup Nagoya #6 in 名古屋城 にて UE4 の AI に関する講演をしてきました。

 

普段は講演の依頼はお断りすることが多いのですが、刹那的に生きているため「会場が城!?すげー!!」などといった理由でお受けすることにしました。

UE4 関連でご活躍されている方々が日本各地から集結していて小さなフェス感がありました。懇親会で色々な (書けない) 話ができて楽しかったので参加してよかったです。

講演で使用したスライドはこちらです。

普通なら気に留めない部分にいちいち着目して検証やソースコードリーディングを行うという謎のスライドになりました。AI 入門的なものを期待されていた方はすみません…。

最後に、会場の約束後で宣伝や告知ができないにも関わらず交通費を出してくれた会社に感謝致します。

[UE4] Behavior Tree の Composite Node を自作する

今回は Behavior Tree の Composite Node の自作方法について解説していきます。

Composite Node を自作しようと思ったのは、Unreal Fest 2016 West にて行われた講演 『スクウェア・エニックスにおける UNREAL ENGINE 4 を用いた人工知能技術の開発事例』 の中で、UE4 にはデフォルトで用意されていない 『Weighted Random (重み付きランダム)』 という Composite Node が使われているのを見つけたのがきっかけです。確率分岐フローを入れたいことは結構多いので…。1

作成方法

非常に簡単で、UBTCompositeNode を継承したクラスを作成するだけです。

エンジンコードに手を入れる必要はありません。また、Editor 上で使用できるようにするための登録的な作業も必要なく、クラスを作れば自動で Behavior Tree の Node 追加メニューに表示されるようになります。

スクウェア・エニックスさんの実装とは異なる点が多々ありますが、Weighted Random Node を簡易実装してみましたので、コードをご覧ください。

(BTComposite_WeightedRandom.h)

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTCompositeNode.h"
#include "BTComposite_WeightedRandom.generated.h"

UCLASS()
class T_BTCOMPNODESELFMADE_API UBTComposite_WeightedRandom : public UBTCompositeNode
{
    GENERATED_UCLASS_BODY()

    int32 GetNextChildHandler(struct FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const;
    virtual FString GetStaticDescription() const override;

#if WITH_EDITOR
    virtual bool CanAbortLowerPriority() const override;
    virtual FName GetNodeIconName() const override;
#endif

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weighted Random")
    float LeftChildSelectingRate;
};

(BTComposite_WeightedRandom.cpp)

#include "BTComposite_WeightedRandom.h"

UBTComposite_WeightedRandom::UBTComposite_WeightedRandom(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
    , LeftChildSelectingRate(1.0f)
{
    NodeName = "Weighted Random";

    OnNextChild.BindUObject(this, &UBTComposite_WeightedRandom::GetNextChildHandler);
}

int32 UBTComposite_WeightedRandom::GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
{
    int32 NextChildIndex = BTSpecialChild::ReturnToParent;

    // 子 Node の数が 2 で、かつこの Node に遷移してきた直後であれば
    if (GetChildrenNum() == 2 && PrevChild == BTSpecialChild::NotInitialized)
    {
        const int32 LeftChildIndex = 0;
        const int32 RightChildIndex = 1;
        NextChildIndex = (FMath::FRand() <= LeftChildSelectingRate) ? LeftChildIndex : RightChildIndex;
    }

    return NextChildIndex;
}

FString UBTComposite_WeightedRandom::GetStaticDescription() const
{
    int32 ChildrenNum = GetChildrenNum();

    // 子 Node の数が 2 ならそれぞれの分岐確率を表示
    if (ChildrenNum == 2)
    {
        float LeftPercentage = LeftChildSelectingRate * 100;
        float RightPercentage = 100 - LeftPercentage;
        return FString::Printf(TEXT("Left : %.2f / Right : %.2f"), LeftPercentage, RightPercentage);
    }

    // 子 Node の数が 2 でないなら警告を表示
    return FString::Printf(TEXT("Warning : Connect Just 2 Children Nodes (Currently %d Node(s))"), ChildrenNum);
}

#if WITH_EDITOR

bool UBTComposite_WeightedRandom::CanAbortLowerPriority() const
{
    // Sequence Node と同様に、子 Node が優先度の低い Node から処理を奪うことができないようにする
    return false;
}

FName UBTComposite_WeightedRandom::GetNodeIconName() const
{
    // Selector Node と同じアイコンを使用
    // 独自のアイコン設定は恐らくエンジン改造が必要
    return FName("BTEditor.Graph.BTNode.Composite.Selector.Icon");
}

#endif

以下、実装している重要なメソッドについて解説します。

GetNextChildHandler

int32 GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const

次に遷移すべき Node の Index を返すメソッドです。Node の遷移の仕方を規定する最も重要なメソッドとなります。

第 2 引数 PrevChild で直前に実行された Node の Index が、第 3 引数 LastResult で直前に実行された Node の結果 (成功・失敗・中断) が得られます。

ここでいう “Node の Index” とは相対的な値で、以下のように定められています。

  • BTSpecialChild::ReturnToParent (-2)
    • 親 Node を指す
  • BTSpecialChild::NotInitialized (-1)
    • 特定の Node を指す値ではない
    • PrevChild がこの値の場合、まだこの Node に遷移してきた直後で、子 Node に一度も遷移していないことを示す
  • 非負の整数
    • 子 Node を指す
    • 優先度が高い方から順に (Behavior Tree 上で左に配置されている方から順に) 0, 1, 2… と振られる

Node の右上に付いている、Behavior Tree 全体での実行順を示す数字とは関係ないので注意してください。

Weighted Random のコードは、子 Node の数が 2 で、かつ この Node に遷移してきた直後であれば、ランダムで左右の子 Node のどちらかの Index を返し、それ以外の場合は親 Node の Index を返します。子 Node 数 2 の場合でのみ機能する割り切ったコードになっています。

CanAbortLowerPriority

bool CanAbortLowerPriority() const

子 Node に付加された Decorator の Observer abortsLower Priority を設定できるかどうかを返すメソッドです。つまり、このメソッドが false を返す場合、子 Node が優先度の低い Node の実行を中断して処理を奪うような動作ができなくなります。

デフォルトで用意されている Composite Node の中では、Sequence Node が false を返す実装となっています。優先順位の低い Node から処理を奪うような動作を許可すると、子 Node の実行順が乱れて “Sequence” でなくなりますし、そのような動作に意味があるとは言えないためです。2

Weighted Random においても、子 Node 間で処理を奪うような動作を許可すると 「指定した確率での分岐」 ではなくなるため、禁止しています。

GetStaticDescription

FString GetStaticDescription() const

Node の Description (Node 名の下に小さく表示されている文字列) を返すメソッドです。

Weighted Random のコードでは、子 Node の数が 2 であればそれぞれの分岐確率を表示し、そうでなければ警告を表示するようにしてあります。

作成した Node を使用する

通常の Composite Node と同様に、Behavior Tree Graph 上で右クリックすると出てくるメニューから選択して配置します。

試しに PrintString を行うだけの Task Node を子として接続し、確率を指定して動作確認してみます。

左の Node に遷移する確率が 30% という設定ですが、上手くいっているようです。

まとめ

  • Behavior Tree の Composite Node はUBTCompositeNode 継承クラスを書くだけで自作できる
  • Node の遷移の仕方を規定するGetNextChildHandler メソッドがキモ

  1. 確率分岐フロー自体は、指定した確率で成功する Decorator と Selector Node の組み合わせで実現できます。ただし、その場合は子 Node に対して Decorator を追加することになり、確率分岐であることが一見してわかりにくいです。確率分岐フローが複数必要な場合は、利便性や視認性向上のために専用の Composite Node を用意する方がよいかと思われます。 
  2. UBTComposite_Sequence::CanAbortLowerPriority() に “don’t allow aborting lower priorities, as it breaks sequence order and doesn’t makes sense” とコメントされています。 

[UE4] Event Graph に最初からある半透明の BeginPlay アレ何なの?

(UE 4.18.1)

この記事は、Unreal Engine 4 (UE4) Advent Calendar 2017 の 10 日目の記事です。

「Blog にはある程度まとまった内容の記事を書こう」と思っていたら億劫になって更新しなくなってしまったので、これからは短めの記事を頻度高めで書いていきたいと思います…。

今回はコレ (↓) について書いていきます。

お名前は何ていうの?

『Ghost Node』 と呼ぶようです。無駄にカッコイイ。

どういう Node が Ghost Node なの?

Node を Ghost Node に変換するメソッド UEdGraphNode::MakeAutomaticallyPlacedGhostNode の中身は以下のとおりです。

(Runtime/Engine/Private/EdGraph/EdGraphNode.cpp)

void UEdGraphNode::MakeAutomaticallyPlacedGhostNode()
{
 EnabledState = ENodeEnabledState::Disabled;
 NodeComment = LOCTEXT("DisabledNodeComment", "This node is disabled and will not be called.\nDrag off pins to build functionality.").ToString();
 bUserSetEnabledState = false;
}

また、Node が Ghost Node であれば true を返すメソッド UEdGraphNode::IsAutomaticallyPlacedGhostNode の中身は以下のとおりです。

(Runtime/Engine/Private/EdGraph/EdGraphNode.cpp)

bool UEdGraphNode::IsAutomaticallyPlacedGhostNode() const
{
    return !bUserSetEnabledState && (EnabledState == ENodeEnabledState::Disabled);
}

以上より、

  • EnabledStateENodeEnabledState::Disabled である
  • bUserSetEnabledStatefalse である

という 2 点を両方満たせば、その Node は Ghost Node であると言えるようです。

ENodeEnabledState::Disabled って何なの?

Node には Compile Option というものが存在しており、これによって Node がコンパイル・実行されるかどうかが決まります。内部的には ENodeEnabledState 型のメンバ変数 EnabledState として保持されます。

Compile Option ENodeEnableState 実行の有無
Enable Compile (Always) Enabled 常に実行される
Enable Compile (Development Only) DevelopmentOnly Shipping Build では実行されない
Disable (Do Not Compile) Disabled 常に実行されない

なお、

Editor Preferences → Content Editors → Blueprint Editor → Experimental → Allow Explicit Impure Node Disabling

のチェックをオンにすることで、Inpure Node  (実行ピンが付いているもの) の右クリックメニューにて Compile Option の変更ができるようになります。

Ghost Node は EnabledStateENodeEnabledState::Disabled なので、常にコンパイル・実行されず、無いもの同然として扱われるということです。

Ghost Node には Disabled と書かれたしましまが付いてないけど何故なの?

Compile Option を Disable に設定した Node には “Disabled” と書かれたしましまの Border が付いていますが、Ghost Node には付いていません。

このしましまの Slate を描画する処理は、SGraphNode::UpdateGraphNode の中にある以下の if ブロック内に書かれています。

(Editor/GraphEditor/Private/SGraphNode.cpp)

if ((GraphNode->GetDesiredEnabledState() != ENodeEnabledState::Enabled) && !GraphNode->IsAutomaticallyPlacedGhostNode())

先に挙げた UEdGraphNode::IsAutomaticallyPlacedGhostNode の内容と合わせると、Ghost Node でしましまが描画されないのは、bUserSetEnabledStatefalse であることに依るものとわかります。

この他にも、bUserSetEnabledState の値によって挙動が変化する処理がいくつかあります。1

不要な Node が化けて出てくるんだけど除霊できないの?

EditorPerProjectUseSettings.ini[DefaultEventNodes] セクションで Ghost Node の追加・削除ができます。

例えば、

  • Actor の ActorBeginOverlap はあまり使わないので Ghost Node として出てほしくない
  • Behavior Tree の Task を書く時に、最初から ReceiveExecuteAI が出ていてほしい

という場合は、

《Project Directory》/Config/DefaultEditorPerProjectUserSettings.ini に以下のように記述します。

[DefaultEventNodes]
-Node=(TargetClass=Actor TargetEvent="ReceiveActorBeginOverlap")
+Node=(TargetClass=BTTask_BlueprintBase TargetEvent="ReceiveExecuteAI")

TargetClass には A や U などの接頭辞を除いた Class 名を、TargetEvent には元のメソッド名 (Display Name ではない) を指定する必要があるようです。

まとめ

  • 最初から配置されている半透明の Node は Ghost Node と呼ぶ
  • Ghost Node は基本的に Disable な Node と同じだが、表示や操作時の挙動など異なる点もある
  • デフォルトで配置される Ghost Node は .ini を編集することで追加・削除できる

***

明日は ほげたつ (@HogeTatu) 大先生による 『UE4でゲームを作る時に考えていること2選』 です。


  1. 例えば、作成しようとした Node が既に配置済だった場合、配置済 Node が Disable の場合はフォーカスが合うだけですが、Ghost Node の場合は Ghost Node が削除されて新規 Node が作成されます。Editor/BlueprintGraph/Private/BlueprintEventNodeSpawner.cppUEdGraphNode UBlueprintEventNodeSpawner::Invoke を参照。 

Blender to UE4 (ver.2016)

この記事は、Unreal Engine 4 (UE4) Advent Calendar 2016 の 4 日目の記事です。

まず始めにですが、来年より UE4 専門会社として有名なあの会社にお世話になることになりました!試用期間中にあっさり切られないように頑張ります!

写真は山のようにいただいたお祝いの食べ物です。この他に書籍などもいただきました。ありがとうございます!

さて、『Blender で UE4 用 3D モデルを作って Import するまでに守るべきことまとめ』 という記事を書いてから 2 年弱になります。検索で上位に引っかかるらしく、未だに閲覧されることの多い記事なのですが、流石に内容が古くなってきたため、改訂版として本記事を書きたいと思……

……っていました。が、すみません!あまりにも遅筆過ぎる上に分量が多いため、記事が完成していません!残りは追々更新していくということで、書いてある部分だけ公開します!
スマヌ _(´ཀ`」 ∠)_

Blender 2.78a、Unreal Engine 4.13.2 を使用します。Blender の方は現段階での最新版ですが、Unreal Engine の方は最新版の 4.14.0 が自分の環境で安定しないため、1 つ前のバージョンを使用しています。

 

用語について

Blender と UE4 では同じような概念・機能・要素に対して違う言葉を使うことがありますので、先でまとめておきます。完全にイコールではない点に注意してください。

Blender での呼び方 UE4 での呼び方
Armature Skeleton
Action Animation
Shape Key Morph Target

『FBX 6.1 ASCII (A)』 ではなく 『FBX 7.4 binary (B)』 を選択する

Blender に標準搭載されている FBX Exporter では、書き出す FBX のバージョンを 『FBX 6.1 ASCII』 と 『FBX 7.4 binary』 の 2 種類から選択することができます。

以前の記事 では、「『FBX 7.4 binary』 のメリットが確認できない上にデメリットがあるので、『FBX 6.1 ASCII』 にしてください」と書きましたが、現在自分は 『FBX 7.4 binary』 を利用しています。その理由について、これから書いていきます。

以下、『FBX 6.1 ASCII』 による書き出しを 『A 書き出し』 または単に 『A』、『FBX 7.4 binary』 による書き出しを 『B 書き出し』 または単に 『B』 と省略して呼ぶことにします。

ここで注意なのですが、例えば「A 書き出しでは○○のデータが書き出されない」と言った場合、それはあくまで「Blender で 『FBX 6.1 ASCII』 オプションを選んだ場合の Export 処理に○○を書き出す処理が含まれていない」という話であって、「FBX 6.1 ASCII というファイルフォーマットが○○の書き出しに対応していない」という話ではありません。その点を留意した上でお読みください。

理由 1 : B はデフォルトで Mesh が正しい方向を向くため

B 書き出しにおいては、特に何も設定しなくても、Import された Mesh は正しい方向を向きます。

A 書き出しの場合、Export 側と Import 側の両方で適切な設定を行わないと、Mesh がうつ伏せに転倒します。しかも、その手法にいくつか問題があることがわかっています。

A において必要となる、Use T0As Ref Pose オプションを利用した Mesh の姿勢制御について

以前の記事 では、A 書き出しにおいて

  • Blender での FBX Export 時に、座標系の Forward-Z ForwardUpY Up に設定する
  • UE4 での FBX Import 時に、Use T0As Ref Pose を有効にする

とすることで、Skeletal Mesh と Animation Sequence の姿勢が正しく設定される、ということを書きました。

しかし、前述のとおり、この手法にはいくつか問題があります。以下で問題点を挙げていきます。

問題点 1 : Mesh の姿勢制御ができないソフトの併用時に別の対策が必要になる

上記手法は Export 側と Import 側の両方で設定を行う必要があるため、Substance Painter のような Mesh の姿勢制御ができないソフトを併用する場合、OBJ で書き出すなどファイルを別に用意する必要があり、管理が手間になります。

問題点 2 : Use T0As Ref Pose オプション有効時に法線の向きがおかしくなるケースがある

UE4 の FBX Import 設定にて Normal Import MethodImport Normals and Tangents にしている場合、Use T0As Ref Pose を有効にして Skeletal Mesh の姿勢を変更すると、法線の向きがおかしくなるというバグが存在します 1

以下の画像は、UE4 の Buffer Visualization Mode で表示させた World Normal の G-Buffer です。右側の Cube が、A で書き出して上記の設定で Import した Mesh です。左の Cube や床と比較すると、法線の向きが正しくないことがわかります。

理由 2 : B では Morph Curve を埋め込んだ Animation を書き出せるため

B 書き出しの場合、Blender 側で予め作成した Shape Key Animation を、Morph Curve として各 Animation Sequence に埋め込むことが可能です。

A 書き出しは Morph Curve の埋め込みに対応していないため、Import 後に UE4 側で Morph Curve を作成する必要があります。

Morph Curve とは?

UE4 では、Animation Sequence に対して Morph Curve と呼ばれるものを追加することができます。これを利用すると、Animation の再生中に Morph Target の値を変更することができます。

例えば、歩行 Animation に Curve を追加し、Curve 名を左目を閉じる Morph Target の名前と一致させ、…

… Curve Type を Morph Curve に設定し、縦軸 0.0 ~ 1.0 の範囲で適当に Key を打つと、…

… 以下のような感じで、歩行の Bone Animation に対して ウインクを行う Morph Target Animation を追加することができます。

以上のように、Morph Curve は UE4 上で作成することが可能なのですが、できればこういったものは Blender 側で作成しておいて、Export / Import すれば自動的に生成される、というのが理想です。B 書き出しではそれが可能となります。

Morph Curve の埋め込み方

Blender にて Driver を用いて Action と Shape Key Animation を連動させることで、B 書き出しの際に Animation Sequence に Morph Curve が埋め込まれるようになります。手順は以下のとおりです。

  1. Armature に対して Shape Key 操作用の Bone を作成する
  2. Shape Key に対して Driver を追加し、1 で作成した Bone の移動・回転・拡縮に合わせて値が変化するように設定する
  3. 1 で作成した Bone に Key を打って、Shape Key Animation を作成する

詳しくは naotaro さんのブログ記事を読んでください。

試しに 「手を振っている最中ににっこり笑う」 Action を作ってみました。頭の上に見える 3 本の線が Shape Key 操作用の Bone で、X 軸回転操作で対応する Shape Key の値が増減するような設定になっています。

これを B で書き出して UE4 に Import すると、以下のようになります。Morph Curve が自動で埋め込まれていることが確認できます。

理由 3 : A と B で UE4 Import 時の Bone 構造が同一になったため

Blender 2.73a の頃は、UE4 に Import した際の Bone 構造が A 書き出しと B 書き出しで異なっていました。しかし、2.78a ではこの差異がなくなったため、A か B かを選択する際にこの点を考慮する必要がなくなりました。

例えば、以下のようなモデルを FBX で書き出すとします。

Blender 2.73a にて A 書き出しを行い、UE4 に Import すると以下のようになります。Blender における Armature Object (Armature_Object) が Root Bone の扱いになっており、本来の Root Bone であった RootBone_Bone はその子になっています。

一方で、Blender 2.73a にて B 書き出しを行うと以下のようになります。RootBone_Bone がそのまま Root Bone として扱われています。

このような差異があったのですが、Blender 2.78a では A・B どちらの場合でも Armature Object が Root Bone として扱われるようになっています。

Root Motion を付ける方法

さて、A と B で Bone 構造が同一となること自体はよいのですが、どちらの場合でも Armature Object が Root Bone として扱われるため、本来の Root Bone に Location の Key を打っても、UE4 上で Root Motion として扱われないという問題があります。

Root Motion を付けたい場合は、Pose Mode ではなく Object Mode にし、Armature Object の Location に Key を打つ必要があります。

UE4 Import 時の Bone 構造が今後変わる可能性について

GitHub の EpicGames/UnrealEngine リポジトリ に対して、Armature Object が Root Bone 扱いとなるのを修正する Pull Request が提出されており、今現在は Waiting For Review いう扱いになっています。

どうやら、FFbxImporter::GetRootSkeleton メソッドが親 Bone を辿って Root Bone を見つける過程で、Attribute が Null の Node まで辿ってしまっているのが原因のようです (Armature Object の Attribute は Null)。

もしこの Pull Request が Merge された場合、普通に Root Bone の Location に Key を打てば Root Motion になるので、上で説明したテクニックは不要になります。

ただしその場合、Root Bone が複数存在する Armature は UE4 に Import することができなくなります。UE4 は単一 Root Bone のみをサポートしているので、親を設定していない Bone を 1 つにする必要があります。以下は、Root Bone を複数持つ FBX を UE4 に Import した際のエラーメッセージです。

以下、執筆中…

今後、以下のようなことを書いていく予定です。

  • 『FBX 6.1 ASCII (A)』 ではなく 『FBX 7.4 binary (B)』 を選択する
    • 理由 4 : B において発生するアニメーションのガタつき問題が対処可能であるため
      • B 書き出し時のみ発生するアニメーションのガタつきを解決する方法の解説
        • Visual Keying で Animation Bake した上で IK Constraint を無効にする
      • NLA Editor を用いた書き出し対象 Action の選択およびトリミング方法の解説
  • Scale 問題
    • [この tweet](https://twitter.com/rarihoma/status/783868446815637509) のリプライチェーンで言っていることをまとめる
      • Unit Scale Factor と Local Scaling について解説
        • どのような操作で Unit Scale Factor や Local Scaling の値が変化するか
          • Apply Unit Scale オプション有効
          • Object の Scale 変更
          • Units → Unit Scale 変更
          • Apply Transform オプション有効
        • 「Export / Import 時に Scale 100 倍」で発生する諸問題について
          • どのような問題が発生するか
            • Persona Animation Editor 上で Bone の回転がおかしくなる現象 (将来解決予定のバグ)
            • Socket の Scale が 100 倍になる現象 (UE4 の仕様)
            • 加算レイヤートラックの Curve による影響が 100 倍になる現象 (UE4 の仕様?)
          • なぜ問題が発生するのか
            • 各計算に Local Scaling の値が考慮されているから
        • Scale 問題の発生しない FBX (Unit Scale Factor = 1, Local Scaling = 1, モデルのサイズ 100 倍) を作成するにはどうすればよいか
          1. 普通に作って Apply Transform をオンにする方法
            • Animation が壊れる可能性あり
          2. Unit Scale = 0.01 にした状態で普通に作る
            • 以下の問題がある
              • Camera の Clipping 設定の変更などが必要
              • 物理シミュが正しく動かない
                • 1 m のものが 1 m 落ちる挙動と、100 m のものが 100 m 落ちる挙動は相似ではない
              • 今後 Blender の Viewport が PBR 対応した場合に見た目がおかしくなる可能性がある
          3. 100 倍 Scale 0.01 の Empty を Armature の親にする
        • 余談 : Scale 変更によって壊れてしまった Action の直し方
  • Smoothing 問題
    • どう設定すればよいのか
      • 参考 : [Exporting Smoothing information from Blender via FBX — polycount](http://polycount.com/discussion/155012/exporting-smoothing-information-from-blender-via-fbx)
        1. Smooth にしたいところに Shade Smooth をかけ、それ以外は Shade Flat にする方法
          • この方法では実現できない形状がある
        2. 全体を Shade Smooth にして Sharp にしたい Edge を Mark Sharp し Edge Split Modifier を適用
        3. 全体を Shade Smooth にして Sharp にしたい Edge を Mark Sharp し Auto Smooth をオンにする (angle = 180)
  • その他
    • Skeletal Mesh の分割 Import は依然として必要

中途半端な形になってしまい申し訳ないのですが、ひとまずこれでバトンを繋ぎたいと思います。

明日は com04 さんの 「UE4 のセルシェード実装方法を纏めたい」 です。


  1. Unreal Engine Issues ではこのバグの Affect Versions は 4.11 と表記されていますが、自分が 第 3 回 UE4 ぷちコンに応募した 『はじけるタワーディフェンス!』 においてもこの現象に悩まされたので、UE 4.7 の時点で既に存在していたバグだと思われます。 

UE4 + FMOD でインタラクティブなサウンド体験を実現しよう!

この記事は、Unreal Engine 4 (UE4) Advent Calendar 2015 の 18 日目の記事です。

昨日の記事は、ntaro さん の「UE4のWindowsにおけるマルチタッチについて」でした。これまで様々なデバイスを UE4 と連携させて作品づくりを行ってきた ntaro さんならではのとても貴重な記事でした。Kingyo いいですね!

さて、この記事では、オーディオミドルウェア FMOD を UE4 と連携させて、ゲームにインタラクティブなサウンド要素を盛り込む方法について解説していきます。まずは、以下の動画をご覧ください。

プレイヤーの正面遠くに配置されているスピーカーから BGM が流れています。プレイヤーがスピーカーに近づくにつれて、BGM を構成する音色が増えていくのがおわかりでしょうか?FMOD を利用すると、ゲーム内のパラメータ (ここでは、Audio Listener と Audio Speaker の距離) に応じてサウンドが変化するような仕組みを簡単に実装できます。

また、スピーカーが BGM の拍に合わせて振動していることに気づくかと思います。FMOD を利用すると、サウンドの拍や任意のタイミングで UE4 の Event を発行することができるので、サウンド主導のインタラクティブな表現を実現できます。

それでは、FMOD の導入手順について説明した後に、上記 2 点の実装方法について解説していくことにします。

なお、使用した UE4 のバージョンは 4.10 です。

FMOD の導入

ライセンス条項の確認

導入作業を行う前に、必ずライセンス条項の確認を行いましょう。記事執筆時点では、FMOD のライセンス条項は http://www.fmod.org/sales/ にて確認できます。

利用形態によっては License Fee が発生しますし、Non Commercial / Educational ライセンスであっても Credit の表示義務などが発生しますので、よく読みましょう。

ソフトウェア・プラグイン のインストール

ファイルのダウンロード

Download ページ から、以下の 2 つをダウンロードします。なお、ダウンロードを行うにはサイトアカウントの作成が必要となります。

  • FMOD Studio Authoring Tool (以下、単に FMOD Studio と呼ぶ)
  • FMOD Studio for Unreal Engine 4 (以下、FMOD Plugin と呼ぶ)

FMOD Plugin は、お使いの UE4 のバージョンに対応したものを選択してください。また、FMOD Studio は、FMOD Plugin のバージョンと一致したものを選択してください。

FMOD Studio のインストール

ダウンロードしたインストーラの指示に従ってインストールしてください。

FMOD Plugin のインストール

ダウンロードした zip ファイルを解凍し、その中にある FMODStudio ディレクトリを UE4 の Plugins ディレクトリに格納してください。

ちなみに自分の環境 (Windows) では、UE4 の Plugins ディレクトリのパスは C:\Program Files\Epic Games\4.10\Engine\Plugins でした。

FMOD Plugin を導入したバージョンの UE4 エディタを起動し、メニューバー → ヘルプ を開いてください。その中に FMOD Help の項目があれば、正常にインストールされています。

以上で導入は完了です。

プロジェクトの設定

UE4 と FMOD Studio を連携させて作業を行うためには、以下の設定を双方のプロジェクトで一致させる必要があります。

  • Output Format : サウンドの出力フォーマット
  • Bank Output Directory : サウンドの出力先

UE4 プロジェクトの作成と FMOD Plugin の設定

UE4 の新規プロジェクトを作成して ツールバー → 設定 → プロジェクト設定… を開き、プラグイン → FMOD Studio を選択して、以下の項目を設定します。

  • Basic → Output Format : 好きな出力フォーマット
  • この記事の説明どおりに作業を行うのであれば、Stereo を選択してください
  • Basic → Bank Output Directory : FMOD
  • Content ディレクトリ以下の任意のパスで構いませんが、特に理由がなければデフォルトの FMOD にしておくのが無難です

Bank Output Directory の作成

上で設定した Basic → Bank Output Directory のパスが存在しない場合は作成する必要があります。デフォルトの FMOD で設定してある場合は、《UE4 プロジェクトのディレクトリ》/Content/FMOD という名前のディレクトリを作成します。

FMOD Studio プロジェクトの作成と設定

FMOD Studio を起動します。起動すると自動的に新規プロジェクトが作成されているかと思いますが、もしサンプルプロジェクトなどの既存プロジェクトが開かれた場合は、メニューバー → File → New を選択して新規プロジェクトを作成してください。

その後、メニューバー → Edit → Preferences... を開き、以下の項目を設定します。

  • Format タブ → Project OUtput Format : UE4 プロジェクトの Basic → Output Format と同じフォーマットにする
  • Build タブ → Build banks output directory(optional) : UE4 プロジェクトの Basic → Bank Output Directory と同じパスにする

以上で設定は完了です。

簡単な連携テスト

本格的なサウンド作成に取り掛かる前に、「FMOD Studio でサウンドファイルを読み込み、それをそのまま UE4 側で再生する」という簡単な連携テストを行って、作業の流れを把握することにしましょう。

FMOD Event の作成

まず、FMOD Studio 上で新規 Event を作成します。Events タブを開き、検索ボックス下の領域を右クリックして New Event を選択してください。Event が作成できたら、Event 名を Test としておきます。

UE4 にも Event はありますが、「呼び出されたら処理を行うもの」という点では FMOD Event も同じです。FMOD Event の処理内容は「サウンドを鳴らすこと」なので、単純に「FMOD Event ≒ サウンド」だと考えても差し支えないかと思います。

次に、Audio 1 Track に、何でもいいのでお手持ちの mp3 ファイルをドラッグ & ドロップしてください。

Event の作成はこれで完了です。

Bank の Build

Event の作成が完了したので、UE4 で利用できるように書き出しを行いましょう。FMOD Studio では、この作業のことを「Bank の Build」と呼びます。Bank とは Event の集合のことです。

先ほど作成した Event の Test は、まだどの Bank にも割り当てられていません (Events タブ内の Test の右側に #unassigned と表示されていることから確認できます)。このままでは Build を行っても書き出されないので、Bank への割り当てを行います。

Events タブ内の Test を右クリックし、Assign to Bank → Master Bank を選択します。

これで Event が Bank に割り当てられたので、Build を行います。メニューバー → File → Build を選択します。

UE4 で FMOD Event を呼び出す

Bank の Build が終わったら、UE4 のコンテンツブラウザで FMOD Event が書き出されていることを確認してください。

確認できたら、FMOD Event をドラッグ & ドロップしてレベルに配置してください。

ゲームをプレイすると、サウンドが鳴っているはずです。鳴らない場合は、プレイヤー (Audio Listener) が FMOD Event に十分近いかどうかと、詳細 → Activation → Auto Activate のチェックがオンになっているかどうかを確認してください。

これで連携テストは終わりです。今後の作業のために、FMOD Event Test はレベル上から削除しておいてください。

Audio Listener との距離に応じてサウンドが変化する FMOD Event の作成

最初にお見せした動画にあった、プレイヤーが近づくにつれて音色が増えていくサウンドを作成してみたいと思います。

動画で使っている自作の音素材 6 つと、ついでにテキトーに作成したスピーカーの 3D モデルを以下に置いておきます。これを使って作業を進めていきます。

AssetsForFMODTest.zip

FMOD Studio での作業

まずは FMOD Studio で新規 Event を作成します。Event 名は BGM としておきます。

次は Audio Track にサウンドファイルを配置します。サウンドファイルは 6 つあるので、Audio Track も 6 つ必要です。どれでもいいので Audio Track を右クリックし、Add Audio Track を選択すれば追加できます。

この Event はループで再生させたいので、そのための設定をします。Audio Track の上にある黒い領域を右クリックし、Add Loop Region を選択します。

Loop Region が追加されるので、ループさせたい範囲を設定します。8 秒ちょうどでループする曲なので、0:00:000 ~ 0:08:000 の範囲を設定します。サウンドが Loop Region から若干はみ出るのは仕様です。

減衰の設定を行います。Master Track を選択し、Distance AttenuationMin &amp; Max Distance を 0 ~ 150 に設定します。FMOD Studio における長さの単位はメートルなので、150 とは 150 メートルのことを指します。「距離が離れたら音色が少なくなる」ことを確認する前にサウンドが減衰してかき消えてしまうと困るので、Max Distance を長めに設定しています。

さて、ここからが設定のキモの部分です。距離のパラメータを導入し、その値に応じて各 Audio Track のボリュームが変化するように設定します。

まずは Timeline タブ右側の + ボタンをクリックし、Add Built-In Parameter → Distance... を選択します。

Distance の値の範囲を設定するダイアログが出るので、0 to 100 に設定します。

作成された Distance タブを選択し、Audio Track のボリュームフェーダー (ツマミ) を右クリックして Add Automation を選択します。これをすべての Audio Track で行います。

これで、「どの距離でどれだけのボリュームを出すか」を、グラフで設定することができるようになりました。Audio Track 上を左クリックで制御点の追加、制御点のドラッグで移動ができるので、以下のように設定します。

これで設定は完了です。BGM Event を Master Bank に割り当てて、Bank の Build を行います。

UE4 での作業

先ほどの連携テストでは FMOD Event をレベルにドラッグ & ドロップすることで配置・再生を行いましたが、今回は FMODAudio Component を Actor に付加させる方法で行ってみます。

まず、スピーカーの 3D モデル (Speaker.fbx) を Skeletal Mesh として Import し、レベルに配置して、Blueprint クラス BP_Speaker に変換します。

BP_Speaker の編集画面を開いて FMODAudio Component を追加し、詳細 → Sound → EventBGM を設定します。

これで、スピーカーの位置から BGM が再生されるようになりました。ゲームをプレイして、スピーカーに近づいたり離れたりしてみてください。距離に応じてサウンドが変化するはずです。

サウンドの拍子に合わせてスピーカーが振動する処理の実装

FMOD Studio での作業

FMOD Event にテンポ情報を埋め込むことで、拍子のタイミングで UE4 の Event を発生させることが可能となります。

Events タブにて BGM を選択し、TimeLine タブを選択状態にします。Audio Track の上にある黒い領域を右クリックし、Add Tempo Marker を選択します。

Tempo Marker が追加され、Timeline のメモリが時間表示から小節数/拍数表示に変更されました。Tempo Marker の位置を先頭に移動させます。また、この曲は bpm 120 かつ 4/4 拍子の曲なので、値を 120 4/4 に設定します。

UE4 での作業

BP_Speaker の編集画面を開き、追加済の FModAudio Component を選択して、詳細 → Callbacks → Enable Timeline Callbacks のチェックをオンにします。

これで、FMODAudio Component の Event である On Timeline MarkerOn Timeline Beat が発行されるようになります。これら 2 つの Event 発行処理は重いらしいので、必要ない場合はオフにしておきましょう。

拍子のタイミングで何らかの処理を行わせたい場合は On Timeline Beat Event の方を利用します。スピーカーの 3D モデルには Morph Target を仕込んであるので、その Weight を拍子のタイミングで変更させることで振動を表現します。ノードは以下のとおりです。

ちなみに、On Timeline Beat ノードを見ればわかるとおり、この Event では Bar (小節数) や Beat (拍数) などの値が取得可能ですが、ここでは利用していません。

これで、拍子に合わせてスピーカーが振動する処理を実装することができました。

最後に

ゲーム特有のインタラクティブ性を映像だけに留めるのはもったいないと感じています。「サウンドは流すだけ」だった人も、これを機にインタラクティブなサウンドの実装に取り組んでみてはいかがでしょうか。

明日は とげとげさん の「UE4のソースコード的な何か」です。UE4 のソースコード周りを解説した Dive to Unreal Engine 4 の著者さんなので、内容に期待大です!また、とげとげさんとは Unreal Engine 4 (UE4) 其の弐 Advent Calendar 2015 21 日目の記事の作成を共同で行うことになっていますので、そちらも合わせてよろしくお願いします。

ボタンを激しく連打するとD言語くんが大暴れする UE4 エディタ拡張

はい。

この記事は、D言語くん Advent Calendar 2015 の 3 日目の記事です。

本当は今年作ったD言語くんの動画やイラストを並べるだけにしようと思っていたのですが、D言語くん Advent Calendar が Qiita に存在する以上はプログラムチックなことを書かねばならん気がした (というか中の人に若干釘刺された) ので、C++ で UE4 のエディタ拡張を書いてみました。

UE 4.9.2 を使用しています。

UE4 のエディタ拡張について

ゲームエンジンのエディタ拡張というと Unity のイメージが強いかと思いますが、UE4 でもできます。

Unreal Fest 2015 にてエディタ拡張に関する講演1があり、YouTube に動画が公開されているので、エディタ拡張でどのようなことができるのか興味がある人は、見ることをオススメします。

上記の動画では、詳細パネル上で Spline を編集できるエディタ拡張について解説されています。グリッドマップエディタも紹介されていますね。ウィジェットの描画部分を細かくいじれるようなので、ほぼ何でもできる気がします。

動画で紹介されたものも、今回自分が作ったものも、すべて「特定の Actor の詳細パネルに何かを追加する」タイプのエディタ拡張ですが、それだけではなく、独自のウィンドウを出すようなこともできます。

コード

ここからは、今回作成した『ボタンを激しく連打するとD言語くんが大暴れするエディタ拡張』のコードについて、簡単に解説していきます。

先ほど紹介した講演の資料 の p.102 以降でエディタコードモジュールの作成方法について解説されているので、それと合わせて読めば、何となくやっていることがわかるはずです。

UE4 プロジェクト DManProject のディレクトリ構造は以下のようになっています。

  • DManProject
    • Content
      • Editor
        • DManImageA.png
        • DManImageB.png
    • Source
      • DManProject
        • DMan.cpp
        • DMan.h
      • DManProjectEditor
        • DManDetailsCustomization.cpp
        • DManDetailsCustomization.h
        • DManDetailsStyle.cpp
        • DManDetailsStyle.h
        • DManProjectEditor.Build.cs
        • DManProjectEditor.cpp
        • DManProjectEditor.h
      • DManProject.Target.cs
      • DManProjectEditor.Target.cs

Source / DManProject.Target.cs, DManProjectEditor.Target.cs

講演資料 p.104~106 のとおり、まずは Unreal Build System の Target File を修正します。

DManProject.Target.cs

using UnrealBuildTool;
using System.Collections.Generic;

public class DManProjectTarget : TargetRules
{
    public DManProjectTarget(TargetInfo Target)
    {
        Type = TargetType.Game;
    }

    public override void SetupBinaries(
        TargetInfo Target,
        ref List<UEBuildBinaryConfiguration> OutBuildBinaryConfigurations,
        ref List<string> OutExtraModuleNames
    )
    {
        OutExtraModuleNames.Add("DManProject");
        if(UEBuildConfiguration.bBuildEditor)             // エディタをビルドするときだけ
        {
            OutExtraModuleNames.Add("DManProjectEditor"); // 追加
        }
    }
}

DManProjectEditor.Target.cs

using UnrealBuildTool;
using System.Collections.Generic;

public class DManProjectEditorTarget : TargetRules
{
    public DManProjectEditorTarget(TargetInfo Target)
    {
        Type = TargetType.Editor;
    }

    public override void SetupBinaries(
        TargetInfo Target,
        ref List<UEBuildBinaryConfiguration> OutBuildBinaryConfigurations,
        ref List<string> OutExtraModuleNames
    )
    {
        OutExtraModuleNames.Add("DManProject");
        OutExtraModuleNames.Add("DManProjectEditor"); // 追加
    }
}

Source / DManProjectEditor / DManProjectEditor.Build.cs

モジュールの依存関係の設定を行います。

DManProjectEditor.Build.cs

using UnrealBuildTool;

public class DManProjectEditor : ModuleRules
{
    public DManProjectEditor(TargetInfo Target)
    {
        PublicDependencyModuleNames.AddRange(new string[] {"Core", "CoreUObject", "Engine", "InputCore", "DManProject"});
        PrivateDependencyModuleNames.AddRange(new string[] {"Slate", "SlateCore", "PropertyEditor"});
    }
}

DManProject.uproject

講演資料 p.110 のとおりに Modules を修正します。

DManProject.uproject

{
    "FileVersion": 3,
    "EngineAssociation": "4.9",
    "Category": "",
    "Description": "",
    "Modules": [
        {
            "Name": "DManProject",
            "Type": "Runtime",
            "LoadingPhase": "Default",
            "AdditionalDependencies": [
                "Engine"
            ]
        },
        {
            "Name": "DManProjectEditor", // 追加
            "Type": "Editor",            // エディタをビルドするときだけ
            "LoadingPhase": "Default"
        }
    ],
    "Plugins": [
        {
            "Name": "FMODStudio",
            "Enabled": true
        }
    ]
}

Source / DManProject / DMan.h, DMan.cpp

これは単に Actor を継承しただけのクラスで、中身はテンプレそのまんまです。
エディタモジュールに含まれるものではないので、DManproject ディレクトリの方に入っています。このクラスのインスタンスの詳細パネルにD言語くんを表示することにします。

DMan.h

#pragma once

#include "GameFramework/Actor.h"
#include "DMan.generated.h"

UCLASS()
class DMANPROJECT_API ADMan : public AActor
{
    GENERATED_BODY()

public:
    ADMan();

    virtual void BeginPlay() override;
    virtual void Tick(float DeltaSeconds) override;
};

DMan.cpp

#include "DManProject.h"
#include "DMan.h"

ADMan::ADMan()
{
    PrimaryActorTick.bCanEverTick = true;
}

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

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

 

Source / DManProjectEditor / DManDetailsStyle.h, DManDetailsStyle.cpp

これは Slate Style と呼ばれるもので、外観 (+ そのためのリソース) を管理します。詳細パネルに画像を出すためには、ここでブラシを作成してセットするという手続きが必要です。

画像ファイルは Content/Editor ディレクトリ以下に格納しています。余談ですが、画像は #D言語くん版深夜の真剣お絵描き60秒一本勝負 で描いたものを加工して作りました。

DManDetailsStyle.h

#pragma once

#include "UnrealEd.h"
#include "SlateStyle.h"

class FDManDetailsStyle : public FSlateStyleSet
{
public:
    FDManDetailsStyle();
    ~FDManDetailsStyle();
};

 

DManDetailsStyle.cpp

 

#include "DManProjectEditor.h"
#include "DManDetailsStyle.h"

FDManDetailsStyle::FDManDetailsStyle() : FSlateStyleSet("DManDetailsStyle")
{
    // ブラシを作成してセット (第一引数の文字列で呼び出して使えるようになる)
    Set("DManImageA", new FSlateImageBrush(FPaths::GameContentDir() / "Editor” / "DManImageA.png", FVector2D(270.0f, 270.0f)));
    Set("DManImageB", new FSlateImageBrush(FPaths::GameContentDir() / "Editor" / "DManImageB.png", FVector2D(270.0f, 270.0f)));

    FSlateStyleRegistry::RegisterSlateStyle(*this);
}

FDManDetailsStyle::~FDManDetailsStyle()
{
    FSlateStyleRegistry::UnRegisterSlateStyle(*this);
}

Source / DManProject / DManDetailsCustomization.h, DManDetailsCustomization.cpp

詳細パネルのカスタムレイアウトを記述します。入力に対する応答もここで設定します。

DManDetailsCustomization.h

#pragma once

#include "DManDetailsStyle.h"
#include "Editor/PropertyEditor/Public/PropertyEditorModule.h"

class FDManDetailsCustomization : public IDetailCustomization
{
private:
    TSharedPtr<FDManDetailsStyle> Style;
    TSharedPtr<SImage> Image;

    bool DancePattern;
public:
    FDManDetailsCustomization();

    static TSharedRef<IDetailCustomization> MakeInstance();
    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override;

    FReply OnButtonClicked();
};

DManDetailsCustomization.cpp

#include "DManProjectEditor.h"
#include "DManDetailsCustomization.h"
#include "Editor/PropertyEditor/Public/DetailLayoutBuilder.h"
#include "Editor/PropertyEditor/Public/DetailCategoryBuilder.h"
#include "Editor/PropertyEditor/Public/DetailWidgetRow.h"

FDManDetailsCustomization::FDManDetailsCustomization() : Style(MakeShareable(new FDManDetailsStyle())) {}

TSharedRef<IDetailCustomization> FDManDetailsCustomization::MakeInstance()
{
    return MakeShareable(new FDManDetailsCustomization);
}

void FDManDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
{
    // "D" という名前のカテゴリを編集
    IDetailCategoryBuilder& DManCategory = DetailLayout.EditCategory("D", FText::GetEmpty(), ECategoryPriority::Important);

    // "D" カテゴリにD言語くん表示用の行を作成
    FDetailWidgetRow& DManRow = DManCategory.AddCustomRow(FText::GetEmpty());

    // 中身の設定 (配置や大きさを設定した Box の中に画像を入れて表示)
    DManRow.WholeRowContent()
    [
        SNew(SBox)
        .HAlign(HAlign_Center)
        .VAlign(VAlign_Center)
        .WidthOverride(270.0f)
        .HeightOverride(270.0f)
        [
            // 画像はボタンを押した時に変更したいので、
            // SImage は SNew ではなく SAssignNew で生成して、後から参照できるようにする
            SAssignNew(Image, SImage)
            .Image(Style->GetBrush("DManImageA"))   // FDManDetailsStyle でセットしたブラシを取得して利用
        ]
    ];

    // "D" カテゴリにボタン設置用の行を作成
    FDetailWidgetRow& ButtonRow = DManCategory.AddCustomRow(FText::GetEmpty());

    // 中身の設定 (ボタンのラベルとクリック時の動作を設定)
    ButtonRow.WholeRowContent()
    [
        SNew(SButton)
        .Text(FString("Dance! Dance! Dance! Dance! Dance! Dance! Dance!"))
        .OnClicked(this, &FDManDetailsCustomization::OnButtonClicked)
    ];

}

// ボタンクリック時に呼び出されるメソッド
FReply FDManDetailsCustomization::OnButtonClicked()
{
    Image->SetImage(Style->GetBrush(DancePattern ? "DManImageA" : "DManImageB"));   // FDManDetailsStyle でセットしたブラシを取得して利用
    DancePattern = !DancePattern;
    return FReply::Handled();
}

 

Source / DManProjectEditor / DManProjectEditor.h, DManProjectEditor.cpp

エディタコードモジュールの本体です。ここで、FDManDetailsCustomization のインスタンスを、ADMan の詳細パネルのカスタムレイアウトとして割り当てる処理を行います。

DManProjectEditor.h

#pragma once

#include "EngineMinimal.h"
#include "UnrealEd.h"

class FDManProjectEditor : public IModuleInterface
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};
~~~
{: .prettyprint .lang-cpp .linenums}

<br />
`DManProjectEditor.cpp`

~~~
#include "DManProjectEditor.h"
#include "DManDetailsCustomization.h"
#include "DMan.h"
#include "Editor/PropertyEditor/Public/PropertyEditorModule.h"

void FDManProjectEditor::StartupModule()
{
    FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
    // FDManDetailsCustomization のインスタンスを ADMan の詳細パネルのカスタムレイアウトとして割り当てる
    PropertyModule.RegisterCustomClassLayout(ADMan::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FDManDetailsCustomization::MakeInstance));

    PropertyModule.NotifyCustomizationModuleChanged();
}

void FDManProjectEditor::ShutdownModule()
{
    FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
    PropertyModule.UnregisterCustomClassLayout(ADMan::StaticClass()->GetFName());
}

IMPLEMENT_MODULE(FDManProjectEditor, "DManProjectEditor");

##

以上のコードをビルドし、UE4 エディタ上で C++ クラス/DManProject/DMan をレベルに配置すれば、詳細パネルにD言語くんが表示されているはずです。ボタンを連打して大暴れさせてください。

最後に

今年はD言語くんの年だったな!

来年もD言語くんの年にしような!!!


  1. この講演の情報は Unreal Engine Forum にまとめられており、プロジェクトファイルも公開されています。プロジェクトファイルはエディタ拡張作成の参考になるのはもちろんですが、ピンボール台を簡単に作成するためのツールとしても使えるので、C++ が書けない人でも楽しめるものになっています。 

UE4 の Construction Script でメンガーのスポンジを作る

メンガーのスポンジ を生成する Blueprint Class “BP_MengerSponge” を作ったので、処理の内容をお見せしたいと思います。Construction Script にて Mesh の生成処理を行っているため、変数 N の値を変更することで、穴を空ける処理を N 回行ったメンガーのスポンジをエディタ上ですぐに確認することができます。

これは少し前に Twitter で公開したネタなんですが、UE4 AnswerHub にて ピンポイントな質問 があったため、記事として残しておきます。

はじめに

以下の画像からわかるとおり、Static Mesh の Cube を大量生成して形を作るアルゴリズムを採用しているため、処理がかなり重いです。再帰処理の学習のために作ったもので、ゲーム内で利用することは想定していません。本来は Procedural Mesh 生成でやるものだと思います。

また、あまり時間が取れないため、解説は少なめにします。「見て写せば動く」程度にスクリーンショットを提供するので、処理の内容はご自身で追ってください。

処理の内容

BP_MengerSponge のコンポーネント・関数・変数の一覧です。const_CubeSize の値は、利用する Static Mesh の Cube の大きさに合わせて 100.0 に設定されています。

const_CubePositionVector の配列で、値は以下のとおりです。

メンガーのスポンジにおける「穴を空ける処理」は、一辺の長さが L である Cube を、一辺の長さが L/3 である Cube 20 個に置き換える処理であるとみなすことができます。const_CubePosition は、その 20 個の Cube それぞれの位置を決定するために使用します。

関数 CreateMengerSponge の概要がこちら。

1 では、引数のローカル変数への格納と、再帰の終了条件判定を行っています。

終了条件を満たした場合、2 で Static Mesh Component の生成を行います。Set Static Mesh ノードの New Mesh に設定されている Cube は、エンジンコンテンツにある 100x100x100 のものです1

続いて 3 で Dynamic Material を作成し、Cube の Static Mesh Component に設定しています。単なる色替え処理なので必須ではないです。個々の Cube において位置に応じて色が変わるようにしているので、メンガーのスポンジ全体ではグラデーションがかかった見た目になります。

終了条件を満たさなかった場合は 4 で再帰させます。上の方で述べた、「一辺の長さが L である Cube を、一辺の長さが L/3 である Cube 20 個に置き換える処理」に該当するのがこの部分です。

このようにしてできた関数 CreateMengerSponge を Construction Script にて呼び出しています。

これで以上です。BP_MengerSponge をレベルに配置して、変数 N の値を変更してみてください。


  1. エンジンコンテンツが表示されていない場合は、コンテンツブラウザ右下の「表示メニュー」をクリックし、「エンジンのコンテンツを表示」にチェックを入れます。 

書籍 『Unreal Engine 4 で極めるゲーム開発』 レビュー

株式会社ボーンデジタル様の書籍レビュアー募集企画に当選し、『Unreal Engine 4 で極めるゲーム開発』 を頂きましたので、レビューしたいと思います。ボーンデジタル様、ありがとうございます。

結論から言うと、現時点で間違いなく最高峰の UE4 入門書であり、UE4 を扱う人であれば全員読んでほしいと思える内容です。

注意

  • 本書の概要は、公式の書籍基本情報ページで確認してください
  • 本書は UE4 の入門書ですが、私は UE4 歴半年であり、全くの初心者が書いたレビューではありません
  • 初版第 1 刷に対するレビューです
  • 2015/07/31 現在、本書に付属する『購入者特典ダウンロードデータ』はデータ制作の遅延により段階的公開とされているため、一部のコンテンツが公開されていない時点でのレビューとなります
    • 公開されているコンテンツ
      • イントロダクション
      • リソース
      • 章単位のプロジェクトデータ
    • 公開されていないコンテンツ
      • 追加ドキュメント
      • ハンズオン動画
      • 最終状態のプロジェクトデータ
      • バージョンエラッタ

以下、良い点と惜しい点を挙げていきます。

良い点

信頼できる制作スタッフ

著者の湊和久氏といえば、GREE で開催された『Unreal Engine 4 ビギナー勉強会』にて、40 分でブロックくずしを作るセッション『Blueprint でさくっとマイゲームを作ってみる』を行った人です。

UE4 の一般公開からまだ一ヶ月という時期に行われたこのセッションは、UE4 や Blueprint を知らない当時の私達に大きなインパクトを与えるものであったと記憶しています。この時期に既に UE4 を使いこなしていた湊氏は、UE4 本の著者として大変信頼できます。UE4 専門のゲーム開発会社である株式会社ヒストリアの佐々木瞬氏が協力している点も、本書の信頼性を高めています。

また、本書の学習用リソースとして提供されている各種アセットは、現役でゲーム開発に携わっている専門のスタッフが制作しています。これについては次項に記述します。

質の高いアセットリソース

ゲームエンジンは様々な職種の人が扱うものですが、大抵の場合、ゲームエンジン本の著者はプログラマであることが多いかと思います (湊氏もリードプログラマという肩書を持っています)。そのためか、ゲームエンジン本に付属する画像・3D モデル・音楽などの学習用アセットリソースは、(頑張って用意した著者の方には大変申し訳ないですが) 質があまり高くないことが多く、そもそも提供されないこともあります。

前述のとおり、本書のアセットは専門のスタッフの手によって制作されたものであり、質が高いと言えます。量も十分で、テクスチャに関しては、Normal Map や Roughness Map、ものによっては Specular Map が用意されていたり、効果音に関しては、足音を交互で鳴らせるようにするために 2 つ 1 セットで用意されていたりします。

物理アセットやマテリアルの調整を行う項で実践的かつ説得力のある解説が展開されるのも、ライティングやポストプロセスマテリアルの効果を実感できるのも、そして何より楽しみながら本書の実践が行えるのも、アセットの質の高さのおかげです。

実際のゲーム制作のワークフローを強く意識した内容

第 1 章において

本書では、それぞれの機能が、実際のゲーム制作において、どの順番でどう使われ、どのように連携するのかをラーニングすることに焦点を置いています。
(p.1)

と書かれているとおり、本書は実際のゲーム制作のワークフローを強く意識した、極めて実践的な内容になっています。この点で、単なるゲームエンジンの機能解説本とは一線を画しています。

本書は、第 4 章を丸々費やして一般的なゲーム制作のフェーズやワークフローについて紹介し、それ以降の章で、ワークフロー上の個々の作業について解説するという構成になっています。解説の都合上、章の順番と作業の順番は一致していませんが、以下のような対応表1が用意されているので、「今自分はワークフロー上のどの作業を行っているのか」を意識しながら学習することが可能です。

また、チームでゲーム制作を行う人達向けの配慮もなされています。以下のように各章のはじめに『職種アイコン』が用意され、「どの職種に当たる人がこの章を読むべきか」が明確になっている他、第 9 章では「エンジニアやレベルデザイナ以外の人間が Blueprint の使い方を覚えることによって、作業の流れがどれだけ改善されるか」が示されています。

丁寧でわかりやすい解説

本書の解説は全体的に親切丁寧でわかりやすいという印象を受けました。本書が扱う範囲はそれなりに広く、様々な概念やエンジンの機能を理解する必要があるのですが、説明の意味が理解できないような箇所は自分にはありませんでした。

解説がわかりやすい理由としては色々なことが考えられます。以下に列挙してみます。

  • 単純に文章がわかりやすいため
    • 例え話や具体例の出し方が適切
    • 補足 (Tips、Note、Column) が豊富
    • 各手順を行う理由がしっかりと説明されているため、納得して作業を行える
    • 作業手順がかなり詳細に記述されている
      • 第 23 章 (本書の 2/3 が終わったあたり) から Blueprint の手順解説が詳細ではなくなりますが、その頃には Blueprint の扱いにある程度慣れているはずなので、問題ないと思われます
  • 各章の目的が明確であるため
    • 各章の内容がゲーム制作ワークフロー上の作業と対応していることに依る
  • UE4 が優秀なエンジンであり、習得が難しい概念の学習を一部スキップできるため

特に最初のうちは丁寧な補足情報が随時入るため、UE4 を初めて触る方でも、詰まったり迷子になったりせずに読み進められるかと思います。

また、第 23 章のビヘイビアツリーの説明が大変わかりやすく、これまで UE4 をバリバリ使ってきた知人たちの間でも、かなり評価が高い部分です。日本語によるビヘイビアツリーの解説自体が未だに少ないということもあり、大変ありがたいです。

バージョンエラッタによる継続的なサポート

UE4 は更新頻度が非常に高いゲームエンジンであり、出版された本の内容がすぐに時代遅れになってしまう可能性があります。

しかし、本書では今後 1 年間に渡ってバージョンエラッタ情報が提供される予定ですので、UE4 の最新バージョンを利用しながら、問題なく本書を読み進めることができそうです2

バージョンエラッタ (VersionEratta.zip) : UE 4.8 以降を使って本書のハンズオンを進めた場合の注意点を簡単にまとめた情報を提供します。2016 年 7 月まで定期更新を続ける予定です。
(p.5)

その他

その他、細かい部分ですが、良いと思った点を列挙していきます。

  • チュートリアルタイプの入門書であるにも関わらず、それなりに網羅性が高い点
  • ひととおりの選択肢を挙げた上で、著者おすすめの方法を理由付きで示している点
    • おすすめの操作方法 (p.27)
    • お勧めのレイアウトは? (p.34)
    • (カスタムイベントと関数を) どうやって使い分ける? (p.324)
  • ゲームエンジン本で比較的ないがしろにされがちな、サウンドに関する記述がしっかりしている点
  • UE4 のハマりどころのひとつである、「エンジン内で統一されていない座標系」に関する言及がある点 (p.217)
  • カバーを取っても表紙がカラーで綺麗である点 (←個人的には重要なポイントです)

惜しい点

誤植の多さ

個人的には「技術書に誤植はつきもの」だと考えていますし、正誤表の提供や更新が早いためそこまで問題視していませんが、誤植が多いか少ないかと言われると若干多いかなという気がしましたので、惜しい点として挙げさせていただきました。

公式の書籍基本情報ページ に正誤表がありますので、確認しながら読み進める必要があります。

なお、2015/07/31 時点で正誤表には載っていない、些細な誤字や誤植をいくつか発見しましたので、以下に挙げておきます。

  • p.1 単純な誤字
    • 【誤】 順番に、昨日トピックごとに紹介する
    • 【正】 順番に、機能トピックごとに紹介する
  • p.17 最近行われた UE4 ドキュメントサイトの仕様変更によって誤りになってしまった部分
    • 【誤】ページの右上に日の丸のアイコンがあれば、クリックすることで日本語訳に~
    • 【正】ページ右上のプルダウンメニューに「日本語」の選択肢があれば、クリックすることで日本語訳に~
  • p.50 図 4.9 の誤植
  • p.134 記述の重複
    • 【誤】 アンリアルエディタに実在する「レイヤ」という名前の機能です。に実在する「レイヤ」という名前の機能です。
    • 【正】 アンリアルエディタに実在する「レイヤ」という名前の機能です。
  • p.274 単純な誤字
    • 【誤】 これまでの章ではすべて UE4 が標準で用意たイベントを使っていました。
    • 【正】 これまでの章ではすべて UE4 が標準で用意たイベントを使っていました。
  • p.369 Blueprint 名の誤り
    • 【誤】1. [PNRabbitPawn] のブループリントエディタに戻り、
    • 【正】1. [PNPawn_Rabbit] のブループリントエディタに戻り、

購入者特典ダウンロードデータ提供の遅延

前述のとおり、2015/07/31 現在、購入者特典ダウンロードデータは一部が未提供の状態です。本書の本来の発売予定日は 2014/11/27 であり、これ以上の発売延期を避けるためにデータの段階的提供という手段に踏み切ったのではないかと推測されるのですが、できればすべてのデータを揃えた状態で発売してほしかったです。

特に、追加ドキュメントは「追加」と銘打ってあるものの実質的には本書の一部であると考えられ、これがない現状の本書は若干尻切れトンボ感があります。

とはいえ、これは時間が解決してくれることですので、そこまで大きな問題ではないと思っています。

総評

色々と書きましたが、最初に述べたとおり、本書は現時点で間違いなく最高峰の UE4 入門書であり、UE4 を扱う人であれば全員読んでほしいと思える内容です。

最近、ドラマ『デスノート』のリュークやレムが UE4 にて描かれていることが明かされた り、PS4 版『ドラゴンクエスト XI』の開発において UE4 が採用されていることが発表される など、UE4 の採用事例を多くみるようになり、ユーザーコミュニティも活性化してきているように感じます。このようなタイミングで本書のような優良な入門書が出版されたことは、大変喜ばしいことだと思います。

UE4 に興味のある方は是非この本を手にとって、UE4 で遊んでみることをオススメします。

おまけ

超有益なペーパーニンジャ裏ワザ情報です。


  1. この対応表には誤植があり、実際には、最終章である第 29 章が『BGM と効果音』で、『ゲームの GUI を構築する』と『パッケージを作成する』の章は本書内に存在しません。恐らく購入者特典ダウンロードデータの追加ドキュメントの方に移動されたのだと思われます。 
  2. ちなみに、似たようなサポートを提供している UE4 本として、出村成和氏著『Unreal Engine 4 の歩き方』(インプレス直販電子版) が挙げられます。 

Blender で UE4 用 3D モデルを作って Import するまでに守るべきことまとめ

(Unreal Engine 4.7.2, Blender 2.73a)

Blender で 3D モデルを作成して FBX として Export し、UE4 に Import するまでの過程で、守らなければならないこと・行った方がよいことについて解説していきます。

ここでは、Bone (Blender: Armature, UE4: Skeleton) と複数の Animation (Blender: Action, UE4: Animation Sequence) を持つキャラクターモデルを題材にします。つまりコイツです。

100 倍 Scale で Mesh を作成する

Blender で UE4 向けのモデルを作るにあたって、絶対に考慮しなければならないのが Scale 単位の違いです。Blender では 1 Unit = 1 m であるのに対し、UE4 では 1 Unit = 1 cm なので、普通にモデルを作成して Export / Import すると、サイズが 1/100 になってしまいます。

これの対処法としては、以下の 4 つが考えられます。

  1. (Blender) 最初から : 100 倍の大きさで Mesh を作成する
  2. (Blender) Export 直前 : (通常の大きさで作っていた Mesh や Armature を) 手作業で 100 倍の大きさにする
  3. (Blender) Export 時 : オプションで 100 倍の大きさにして Export する
  4. (UE4) Import 時 : オプションで 100 倍の大きさにして Import する

結論から言うと、現状では 1 の方法で対処するのが無難だと思われます。2 は特に Action の修正が地獄なので避けるべきです。また、3 と 4 では以下のような問題が発生することを確認しています。

  • Skeleton 編集モードにおいて、回転に必要となる角度まで 100 倍になる問題
  • PhysicsAsset の自動作成が上手くいかない問題
  • 3 の場合、本来の 100 倍のサイズで作成されてしまう
  • 4 の場合、エラーが発生して作成されない

さて、1 の方法で Mesh の作成を行うのに、いちいち「123 m のものを作りたいから、100 倍して 12.3 km に…」などと計算するのは面倒です。Blender の Scale 単位を UE4 に合わせることで、この問題を解決します。

Properties Editor → Scene → Units にて MetricDegrees を選択し、Scale0.01 に設定します。

これにより、頂点の座標や辺の長さが実際に作成されているデータの 1/100 で表示されるようになります。つまり、この設定を行えば、Blender 上で 1 m とされるもの (実際のデータは 100 m) は UE4 上でも 1 m になるので、Scale の問題を気にする必要がなくなります。

作成済オブジェクトの実データの Scale を変更するものではないので注意です。Mesh 作成開始前に設定しないと意味を成しません。

この設定を行った後、以下のように遠くの Object や Grid が表示されなくなった場合は、クリッピング距離を変更する必要があります。

3D View → Properties (N を押すと出てくるアレ) → View → ClipStartEnd の値を適切な値に変更します。普通は既に入っている値の 100 倍に設定すればよいはずです。

遠くの Object の表示を直すには End を変更するだけでよいのですが、Start の値が小さすぎる場合も表示の不具合を引き起こすことがあるので、両方設定し直すのがよいでしょう。

なお、Camera にも (3D View のものとは別に) クリッピング距離の設定があるので、Camera を使っているのであれば変更します。設定場所は Properties Editor → Camera → Lens → Clipping です。

キャラクターの正面が -Y 方向、上が Z 方向を向くように Mesh の作成を行う

Blender で普通に Mesh の作成を行えば、この向きになるはずです。

Scale とは異なり、座標系の違いに関しては Export / Import 時に対処すればよいので、Mesh 作成時にわざわざ変な向きで作ったりする必要はありません。

Object の Transform をニュートラルにする

基本的な話ですが、Object (Mesh 及び Armature) の Transform は最終的にはそれぞれ

  • Location : (0, 0, 0)
  • Rotation : (0, 0, 0)
  • Scale : (1, 1, 1)

となるように Apply (Ctrl + A) もしくは Clear (Alt + G, Alt + R, Alt + S) してください。

スムージング設定の表示反映のための Edge Split Modifier は Export 前に削除する

Blender において、スムージングの設定と表示への反映を行うには、以下の手順を実行します。

  1. シャープにしたい辺を選択して Ctrl + E → Mark Sharp を実行し、Sharp Edge に設定する
  2. Mesh のすべての面を選択し、Ctrl + F → Shade Smooth を選択する
  3. Mesh に対して Edge Split Modifier を Stack し、Edge Angle のチェックをオフに、Sharp Edges のチェックをオンにする

これで、Sharp Edge に設定された辺のみがシャープに表示され、その他の辺はスムーズに表示されるようになります。

実は、Export する FBX にスムージンググループ情報を含めたいだけであれば、手順 3 を実行する必要はありません。Blender では Sharp Edge の設定を行うだけでは見た目に変化がないので、表示反映のために手順 3 を実行する必要があるというわけです。

FBX を Export するという目的からすると、手順 3 は Edge Split Modifier によって Mesh の頂点数を無駄に増やす作業でしかありません。スムージング設定の表示確認が終わったら、Edge Split Modifier は削除してしまいましょう。

次項で Edge Split Modifier を削除すべきもう 1 つの理由について解説します。

Shape Key (Mooph Target) を利用する場合は、頂点数の増減を行う Modifier を Stack したままにしない

頂点数の増減を行う Modifier を FBX Export 時のオプションで Apply すると、Shape Key が書き出されない1ようです。書き出されない場合でも特に警告メッセージが表示されたりしないので、原因がわかりにくかったりします。

頂点数の増減を行う Modifier としては、Subdivision Surface Modifier や、前項で言及した Edge Split Modifier などが挙げられます。これらの Modifier は Export 前に手動で Apply するか、不要であれば削除しましょう。

頂点数が維持される Modifier に関しては、Stack したままで問題ないようです。Lattice Modifier の FBX Export 時 Apply で Shape Key が書き出されていることを確認しています。

Shape Key の Value は Export 前にすべて 0 にする

Shape Key の Value0 以外の値にした状態で Export すると、その状態が Value = 0 として扱われるようになってしまいます。Export 前に必ず 0 に設定しておきましょう。

Texture の幅と高さは 2 のべき乗にする

Texture の幅と高さが 2 のべき乗になっていないと、UE4 上で Mipmap が作成されなくなり、これに伴って Texture Streaming2 も行われなくなります。

特別な理由がない限り、2 のべき乗サイズで作成しましょう。なお、正方形である必要はないようです。

すべての Action に Fake User を設定する

Blender では 1 つのプロジェクトファイル (.blend) に複数のアニメーション (Action) を詰め込むことができますが、Armature の Animation Data Container から参照される Action は一度に 1 つだけです。

そして、Blender では、他から全く参照されていない要素はプロジェクトファイルに保存されません。つまり、プロジェクトファイルを閉じたが最後、今参照されている 1 つを除く残りすべての Action は消えてしまいます。

Animation Data Container から参照されていない Action もプロジェクトファイルに保存するには、Fake User の設定を行う必要があります。Dope Sheet → Action Editor → Browse Action to be linked で Action を選択し、F ボタンを押します。これをすべての Action に対して行います。

こうすれば、「Fake User がすべての Action を参照している」という扱いになるため、すべての Action がプロジェクトファイルに保存されます。

Material や Bone などの名前に日本語を使わない

Material や Bone、Shape Key (Morph Target) などの名前に日本語を使うと、現時点では UE4 への Import ができなくなったり、その他不具合が発生したりするようです。日本語名を付けるのは避けましょう。

Blender を日本語 UI で使っている人は、ファイル → ユーザー設定… → システム → 翻訳 → 新規データ をオフにしておきましょう。これがオンだと、新規作成した Material などのデフォルト名が日本語になってしまい、修正の手間がかかります。

なお、UE 4.8 からは、FBX ファイル内の日本語サポートが行われる予定とのこと。

FBX Export 設定を適切に行う

自分が使っている Blender FBX Exporter の設定は以下のとおりです。

設定の中で一番重要なのが、FBX の Version 指定です。絶対FBX 6.1 ASCII にしてください。

もう片方の Version である FBX 7.4 binary を選ぶメリットは今のところ確認できていません。FBX 6.1 ASCII では問題なく Export できるデータでも、 FBX 7.4 binary で Export するとアニメーションがガタついたり、壊れた FBX が書き出されたりするケースを確認しています。また、書き出される Bone 構造が両者では異なる3ようです。

Selected Objects のチェックをオンにした場合は、Export したい Armature と Mesh の両方を選択してから Exporter を開く必要があります。Mesh 作成のために補助的な Object を利用していたり、参考として他のキャラクターの Object を Append している場合などは、このオプションをオンにして Export 対象を指定できるようにする必要があります。

座標系設定は -Z Forward, Y Up にします。これで、Animation Sequence が正しい向きで作成されます。Skeletal Mesh の向きはこのオプションの影響を受けません。こちらに関しては UE4 での Import 時の設定で正しい向きにします。

Sharp Edge の指定を行っているという理由で SmoothEdge にしていますが、Face にしても結果に変化がないような気が…? Off にすると UE4 での Import 時に以下のような警告が出るので、スムージング処理が必要なくても EdgeFace かのどちらかにしておきましょう。

自分は Default Take 相当のものを Action として作っているので、Default Take のチェックはオフにしています。

FBX Import 設定を適切に行う

(Shape Key (Mooph Target) を利用する場合は、次項の設定で Import してください)

自分が使っている UE4 の FBX Import 設定は以下のとおりです。

Mesh → Use T0As Ref Pose のチェックをオンにすれば、Blender にてキャラクターの正面が -Y 方向、上が Z 方向を向くように Mesh を作成した場合において、Skeletal Mesh が正しい方向を向きます。オフだとコケます。

複数作成した Animation の再生時間がそれぞれ異なる場合は、Animation → Animation LengthAnimated Time に設定する必要があります。

Texture はどのみち後から追加で Import する必要があるので、Material → Import Textures のチェックはオフにしてありますが、オンでも特に問題ないと思われます。

Shape Key (Mooph Target) を利用する場合は、Import を 2 回に分けて行う

前項のように一度にすべての要素の Import を行うと、Mooph Target に不具合が発生する場合があるようです。

この現象を回避するために、Import を 2 回に分けて行います。

まず、以下のような設定で Import を行います。Mesh → Import Mooph Targets のチェックをオン、Animation → Import Animations のチェックをオフにします。その他は前項で示したものと同じです。

これで、Skeletal Mesh、Material、PhysicsAsset、Skeleton が Import されました。

次に、Animation Sequence の Import を行います。Mesh → Import Mesh のチェックをオフにし、Mesh → Skeleton に先ほど Import した Skeleton を指定します。

これで、Animation Sequence が追加で Import されました。各 Animation Sequence において Mooph Target の Weight を変更し、異常がないことを確認してみてください。

終わりに

誰かが作った MMD モデルやユニティちゃんを Import して使うのも楽しいですが、Blender のような素晴らしい 3DCG ソフトウェアが無料で使えるわけですから、自分でモデルを作って使う人がもっと増えてもよいのではないか、と思っています。

これまで見てきたように、Blender で作ったモデルを UE4 に持ち込むまでの過程には、様々な注意点・ハマりどころ・罠・ が存在します。これらを回避し、皆さんの自作モデルを UE4 上で活躍させるために、この記事を活用していただければ幸いです。


  1. > Currently shape keys will only be written if modifiers are disabled, or if the modifiers keep the same number of vertices (Extensions:2.6/Py/Scripts/Import-Export/Autodesk FBX – BlenderWiki より) 
  2. > テクスチャストリーミングは、テクスチャをミップマップレベルが高い(粗い)ものから表示して徐々に高精細にすることでロード時間を短縮しようとするもの (テクスチャのインポートについて – ゲームエフェクトデザイナーのブログ (新) より) 
  3. FBX 6.1 ASCII では Armature と同じ名前の Bone が Root Bone として作成されるので、Armature を複数作成していない限りは単一 Root Bone 構造になります。FBX 7.4 binary では Armature の子要素 (子孫要素ではなく直接の子) の Bone が Root Bone として設定されるので、子要素が複数ある場合は複数 Root Bone 構造となり、単一 Root Bone 構造のみをサポートする UE4 では Import に失敗します。 

UE4 にて特定の Actor に対して輪郭線・クリース線・色の境界線を描画する、あるいはD言語くんを真の姿にする方法

「UE4 にインポートしたD言語くん1の 3D モデルに線を付けたい!」ということで、Custom ノードを使ったポストプロセスマテリアル (以下、長いので PPM と略す) を作成しました。これについて解説していきます。

始めに

前提知識

以下に挙げる おかず@pafuhana1213 さんの記事を読んでください (丸投げ)。

これらを読めば、「そもそも PPM って何?」という人でも、輪郭線の描画を行う Custom ノードを使った PPM を、ロジックを理解した上で作成できるようになるはずです。

注意

Custom ノードは、現状 (4.6.2 & 4.7.1 preview 8) ではかなり不安定です。Custom ノードを使った PPM を作成する際には、以下を守ることをオススメします。

  • テスト用プロジェクトで作業を行い、問題がないことを確認してから本番プロジェクトに持ち込む
  • 実行時エラーを引き起こす PPM を作成してしまうと、最悪の場合プロジェクトが編集不能になるため (具体例は後述)
  • Custom ノードの Code テキストボックスにおいて、範囲選択を伴う操作を行わない
  • 超高確率でエディタがフリーズするため
  • 別のテキストエディタでコード編集を行う (範囲選択操作抜きのコード編集は地獄であるため)
  • コードはテキストファイルとして保存しておく (範囲選択を伴う Code のコピーができないため)
  • Code を更新する際は、リセットボタンを押してからコードをペーストする (全範囲選択による Code 全消去ができないため)

解説

少しずつ処理を足しながら解説をしていくので長いです。結果のコードだけ知りたい人は、最後まで飛ばして読んでください。

SceneDepth を用いて『輪郭線』を描画する

おかず@pafuhana1213 さんの記事 にて SceneDepth を用いて輪郭線を描画する PPM が解説されているので、まずはここから始めたいと思います。改変を加えていますが、やっていることの大筋は同じです。

/* DrawLine の Code */

const float1 laplacian8DirFilter[9] = {-1.0, -1.0, -1.0, -1.0, 8.0, -1.0,- 1.0, -1.0, -1.0};

float1 invLineSD, filResSD; /* SD : SceneDepth */

for(int i = -1; i <= 1; ++i) {
    for(int j = -1; j <= 1; ++j) {
        float2 currentUV = centerUV + float2(i, j) * invSize;
        FScreenSpaceData ssd = GetScreenSpaceData(currentUV, false);

        filResSD += smoothstep(0, depthMax, ssd.GBuffer.Depth) * laplacian8DirFilter[(i+1)+(j+1)*3];
    }
}

invLineSD = floor(clamp((abs(filResSD) + strengthSD), 0, 1));

return float4(CalcSceneColor(centerUV), 0) * (1 - invLineSD);

strengthSD という調整用パラメータを導入し、線が描画される箇所を増減できるようにしています。

作成した PPM を、以下のようなレベルに適用すると…

以下のようになりました。

strengthSD = 0 では線がほとんど描画されません。strengthSD = 0.99 とすると改善されますが、足の部分には線があまり描画されていません。これは、足と床の SceneDepth 値が近いため、エッジが検出されにくいからです。

strengthSD = 0.9999 とすると足の部分にも線が描画されるようになりますが、余計な線が描画されるようになり、動かしてみると汚さが目立ちます。床の一部も真っ黒です。

CustomDepth を用いて『輪郭線』を描画する

  • 周囲の深度に依らず、常に一定の輪郭線がほしい
  • D言語くん以外には線は不要

ということで、SceneDepth の代わりに CustomDepth を使って輪郭線を描画することにします。

D言語くんの Actor を選択し、詳細タブ → Render → Render Custom Depth のチェックをオンにします。

これで、D言語くんの深度情報のみが、CustomDepth に書き出されるようになります。

PPM を CustomDepth 仕様に書き換えます。CustomDepth の値は、FScreenSpaceDataGBuffer.CustomDepth で取得できます。

/* DrawLine の Code */

const float1 laplacian8DirFilter[9] = {-1.0, -1.0, -1.0, -1.0, 8.0, -1.0,- 1.0, -1.0, -1.0};

float1 invLineCD, filResCD; /* CD : CustomDepth */

for(int i = -1; i <= 1; ++i) {
    for(int j = -1; j <= 1; ++j) {
        float2 currentUV = centerUV + float2(i, j) * invSize;
        FScreenSpaceData ssd = GetScreenSpaceData(currentUV, false);

        filResCD += smoothstep(0, depthMax, ssd.GBuffer.CustomDepth) * laplacian8DirFilter[(i+1)+(j+1)*3];
    }
}

invLineCD = floor(clamp((abs(filResCD) + strengthCD), 0, 1));

return float4(CalcSceneColor(centerUV), 0) * (1 - invLineCD);

結果は以下のようになりました。

Sobel Filter を用いてエッジ検出を行う

上の結果では少々線が細い気がします。エッジ検出に用いる Filter を Laplacian から Sobel に変更します。

/* DrawLine の Code */

const float1 sobelFilterH[9] = {-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0};
const float1 sobelFilterV[9] = {-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0};

float1 invLineCD; float1 filResCDH, filResCDV; /* CD : CustomDepth */

for(int i = -1; i <= 1; ++i) {
    for(int j = -1; j <= 1; ++j) {
        float2 currentUV = centerUV + float2(i, j) * invSize;
        FScreenSpaceData ssd = GetScreenSpaceData(currentUV, false);

        float1 CD = smoothstep(0, depthMax, ssd.GBuffer.CustomDepth);

        filResCDH += CD * sobelFilterH[(i+1)+(j+1)*3];
        filResCDV += CD * sobelFilterV[(i+1)+(j+1)*3];
    }
}

invLineCD = floor(clamp((pow(filResCDH, 2) + pow(filResCDV, 2) + strengthCD), 0, 1));

return float4(CalcSceneColor(centerUV), 0) * (1 - invLineCD);

若干ではありますが、線が太くなりました。

WorldNormal を用いて『クリース線』を描画する

輪郭線だけでは線が足りないので、クリース線 (折り目部分の線) の描画処理を加えます。クリース部分の検出には WorldNormal を利用します。この値は FScreenSpaceDataGBuffer.WorldNormal で取得できますが、以下の 2 点に注意を払う必要があります。

特定の条件を満たさないと、WorldNormal の取得時にエディタが落ちる

理由はよくわからないのですが、Scene Texture IdWorldNormal が設定されている SceneTexture ノードがどこかで使われていないと、FScreenSpaceDataGBuffer.WorldNormal を取得する際にエディタが落ちます。

これが起こると、最悪の場合 プロジェクトを開く → PPM の処理が走る → エディタが落ちる のコンボが発動し、プロジェクトを一切編集できなくなります。その場合は、プロジェクトディレクトリから PPM の uasset を削除する必要があります。

WorldNormal は 3 次元ベクトル

法線情報は (x, y, z) の 3 次元ベクトルなので、深度情報と同じようには処理できません。最終的には何らかの方法で、線を書くか書かないか (あるいはその中間) という 1 次元情報に直す必要があります2

####

以上に注意して、クリース線の描画処理を追加します。

/* DrawLine の Code */

const float1 sobelFilterH[9] = {-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0};
const float1 sobelFilterV[9] = {-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0};

float1 invLineCD; float1 filResCDH, filResCDV; /* CD : CustomDepth */
float1 invLineWN; float3 filResWNH, filResWNV; /* WN : WorldNormal */
float1 invLine;

for(int i = -1; i <= 1; ++i) {
    for(int j = -1; j <= 1; ++j) {
        float2 currentUV = centerUV + float2(i, j) * invSize;
        FScreenSpaceData ssd = GetScreenSpaceData(currentUV, false);

        float1 CD = smoothstep(0, depthMax, ssd.GBuffer.CustomDepth);
        float3 WN = ssd.GBuffer.WorldNormal;

        filResCDH += CD * sobelFilterH[(i+1)+(j+1)*3];
        filResCDV += CD * sobelFilterV[(i+1)+(j+1)*3];
        filResWNH += WN * sobelFilterH[(i+1)+(j+1)*3];
        filResWNV += WN * sobelFilterV[(i+1)+(j+1)*3];
    }
}

invLineCD = floor(clamp((pow(filResCDH, 2) + pow(filResCDV, 2) + strengthCD), 0, 1));
invLineWN = floor(clamp((dot(filResWNH, filResWNH) + dot(filResWNV, filResWNV) + strengthWN), 0, 1));

invLine = invLineCD + invLineWN;

return float4(CalcSceneColor(centerUV), 0) * (1 - invLine);

法線の x, y, z それぞれの要素についてフィルター処理を行った後、各要素を 2 乗して加算することで、線の情報に変換しています。「各要素を 2 乗して加算」という処理は、内積関数 dot を用いて行っています。

結果はこのようになりました。

SceneColor を用いて『色の境界線』を描画する

まだ目の辺りの線が足りないので、SceneColor を用いた色の境界線の描画処理を加えます。この辺りまで来ると「テクスチャでやれ」感が出てきますが、D言語くんは塗りがハッキリしているので、今回は PPM で実現したいと思います。SceneColor の取得は CalcSceneColor 関数で行います。

/* DrawLine の Code */

const float1 sobelFilterH[9] = {-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0};
const float1 sobelFilterV[9] = {-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0};

float1 invLineCD; float1 filResCDH, filResCDV; /* CD : CustomDepth */
float1 invLineWN; float3 filResWNH, filResWNV; /* WN : WorldNormal */
float1 invLineSC; float3 filResSCH, filResSCV; /* SC : SceneColor */
float1 invLine;

for(int i = -1; i <= 1; ++i) {
    for(int j = -1; j <= 1; ++j) {
        float2 currentUV = centerUV + float2(i, j) * invSize;
        FScreenSpaceData ssd = GetScreenSpaceData(currentUV, false);

        float1 CD = smoothstep(0, depthMax, ssd.GBuffer.CustomDepth);
        float3 WN = ssd.GBuffer.WorldNormal;
        float3 SC = CalcSceneColor(currentUV);

        filResCDH += CD * sobelFilterH[(i+1)+(j+1)*3];
        filResCDV += CD * sobelFilterV[(i+1)+(j+1)*3];
        filResWNH += WN * sobelFilterH[(i+1)+(j+1)*3];
        filResWNV += WN * sobelFilterV[(i+1)+(j+1)*3];
        filResSCH += SC * sobelFilterH[(i+1)+(j+1)*3];
        filResSCV += SC * sobelFilterV[(i+1)+(j+1)*3];
    }
}

invLineCD = floor(clamp((pow(filResCDH, 2) + pow(filResCDV, 2) + strengthCD), 0, 1));
invLineWN = floor(clamp((dot(filResWNH, filResWNH) + dot(filResWNV, filResWNV) + strengthWN), 0, 1));
invLineSC = floor(clamp((dot(filResSCH, filResSCH) + dot(filResSCV, filResSCV) + strengthSC), 0, 1));

invLine = invLineCD + invLineWN + invLineSC;

return float4(CalcSceneColor(centerUV), 0) * (1 - invLine);

結果はこちら。

CustomDepth を用いて線のマスキングを行う

D言語くん以外のオブジェクトにもクリース線と色の境界線が描画されてしまっているので、マスキングを行います。CustomDepth をマスクとして利用します。

/* DrawLine の Code */

const float1 sobelFilterH[9] = {-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0};
const float1 sobelFilterV[9] = {-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0};

float1 invLineCD; float1 filResCDH, filResCDV; /* CD : CustomDepth */
float1 invLineWN; float3 filResWNH, filResWNV; /* WN : WorldNormal */
float1 invLineSC; float3 filResSCH, filResSCV; /* SC : SceneColor */
float1 invLine;

for(int i = -1; i <= 1; ++i) {
    for(int j = -1; j <= 1; ++j) {
        float2 currentUV = centerUV + float2(i, j) * invSize;
        FScreenSpaceData ssd = GetScreenSpaceData(currentUV, false);

        float1 CD = smoothstep(0, depthMax, ssd.GBuffer.CustomDepth);
        float3 WN = ssd.GBuffer.WorldNormal;
        float3 SC = CalcSceneColor(currentUV);

        filResCDH += CD * sobelFilterH[(i+1)+(j+1)*3];
        filResCDV += CD * sobelFilterV[(i+1)+(j+1)*3];
        filResWNH += WN * sobelFilterH[(i+1)+(j+1)*3];
        filResWNV += WN * sobelFilterV[(i+1)+(j+1)*3];
        filResSCH += SC * sobelFilterH[(i+1)+(j+1)*3];
        filResSCV += SC * sobelFilterV[(i+1)+(j+1)*3];
    }
}

invLineCD = floor(clamp((pow(filResCDH, 2) + pow(filResCDV, 2) + strengthCD), 0, 1));
invLineWN = floor(clamp((dot(filResWNH, filResWNH) + dot(filResWNV, filResWNV) + strengthWN), 0, 1));
invLineSC = floor(clamp((dot(filResSCH, filResSCH) + dot(filResSCV, filResSCV) + strengthSC), 0, 1));

invLine = invLineWN + invLineSC;
/* マスキングを行い、CustomDepth 書き出し領域内の線だけを残す */
invLine *= (1 - smoothstep(0, depthMax, GetScreenSpaceData(centerUV, false).GBuffer.CustomDepth));
invLine += invLineCD;

return float4(CalcSceneColor(centerUV) * (1 - invLine), 1.0);
  1. クリース線と色の境界線を合成する
  2. マスク処理を行い、D言語くんのみに線を残す
  3. 輪郭線を合成する

という順番で処理しています。マスク処理の後に輪郭線の合成を行わないと、線が細るので注意です。

これで完成しました!

実際に動いている様子はこんな感じです (1080p 60fps でお楽しみください)。

終わりに

素晴らしい記事を書いてくださった おかず@pafuhana1213 さんに感謝致します!


  1. プログラミング言語Dの公式マスコットキャラクター (Overview – D Programming Language)。可愛い (確信)。 
  2. 色トレスなどを実現したいのであれば、この限りではありません。