NEUROMANTIC

自分でC/C++/UE4/Graphics/ゲームなどをやったことをメモするブログ

超簡単な炎シェーダーを実装してみた。

youtu.be

今更ですが、「Shade」というiOSのシェーダー作成アプリケーションを使用して超簡単な炎を作ってみました。「Shade」というアプリケーションにはHLSLまたはGLSLのdiscardとか、lerpmix)という関数の機能を提供してくれるノードがないため、ちょっと余計に複雑になりました。ですけどノイズマップとグラデーションを使用して類似乱数みたいな表現が出来たという基本的なテクニックは様々なところに使えそうだから良いですね。

Shadeを使用した実装

ノイズマップを使用した炎は以下の順番で実装が出来ます。

  1. ノイズマップと、グラデーションマップを準備。炎が上に燃え立つよう、ノイズモップのUVのV部分は上へ移動するように。
  2. ノイズマップを基準として(y)、step関数を行う。値が01へと分かれる。
  3. 色を付けることで一番簡単な炎が出来上がる。
  4. ノイズマップの値を上げることで、step以後の炎の分布はどんどん下へとさがる。これを利用して段階的に色をつけて、lerp(線形補間)かsmoothstepで炎にグラデーションをつけることも可能。
  5. 炎ではない部分はdiscardで着色を取り消す。(Shadeアプリではdiscardがない為、単に黒い色に。)

具体的な実装手順

f:id:neuliliilli:20190518151559p:plain
最初の部分

スクリーンスペースのScaledされた座標をUVとして扱います。しかしノイズマップを上へ移動させるため、Timeノードから(秒単位で値を返す)少数部分を取り出し、Vへ足すことでノイズマップを縦移動させます。

f:id:neuliliilli:20190518155918p:plain
色付けのためにノイズの部分の取り出しのロジック

Stepを使用してノイズの一部分を取り、そして引き算をさせることで赤、オレンジ、そして黄色になる部分を取り出します。Subtract One Minus Subtract順に赤色、黄色、オレンジ色の配置となります。

f:id:neuliliilli:20190518160603p:plain
色をつけるロジック

線形補間を使用して色を付けます。まずは、赤色と黄色を付けてから、そして混合したものをまたオレンジ色と線形補間をして最終的に混合させます。

f:id:neuliliilli:20190518160737p:plain
類似`discard`の部分

Shadeアプリケーションではdiscardノードがないため、炎ではない部分をただの黒に塗り替えてから色を表示させます。

f:id:neuliliilli:20190518160856p:plain

これで3段階で色がある炎が実装できました。もっと複雑な模様で炎を出すためには、マスクマップを使用してマクスされていない部分を取り除けば良くなりそうと思います。

「2D多角形の内部、外部判定のやり方」のメモ

bowbowbow.tistory.com

判定とは一体

f:id:neuliliilli:20190518105001p:plain
Aは外部にあって、Bは内部にあることが分かる。

  • 2Dの多角形に対し、AまたはBみたいな点が図形の内部にあるか外部なのかを判別することを示す。

図形の内部に位置する点の特徴は

f:id:neuliliilli:20190518105524p:plain
Aは交点が偶数。Bは奇数のことがわかる。

  • 図形の外部から点を通過するRay(光線)を放って、点Pに到達するまでの図形の交点の数を求めば、その点が図形の内部にあるかないかが分かる。
    • 簡単ながらもすごいアルゴリズム。ちょっと変形すれば3Dの三角形トポロジーのメッシュにも適用が出来る。

こっちでは上の図とは違って、左から右へと放つRayを使用して交点を求めます。だとしても外部にある点は偶数で、内部にある点は奇数ということは変わりません。

実装

2Dの場合、ある点Pが図形の中にあるかを判断するために放つRayはXかY軸に並行したものとすれば実装が簡単になります。(3Dはちょっと複雑になります)

  • Rayのスターティングポイントから点Pまでの直線を「半直線」という。
  • 図形を成す全ての線分と、
    1. 半直線が「交差」して、
    2. 点PよりX座標値が小さい「交点」があるかを判断する(この場合、RayはX軸に平行するとする)

条件1が当たれば、「交点」は必ず存在します。しかしその点が点Pの前にあるか、後ろにあるかがわかりませんので位置の値を求めて判断しなければなりません。

以下のコードは2D図形に対してpoint点Pが図形の内部にあるかないかを判断し、内部にあったらtrueを返す関数です。

bool IsInside(const std::array<DVector, 10>& polygon, const DVector& point)
{
    int intersected = 0;
    for (int i = 0; i < 10; ++i)
    {
        const auto& start = polygon[i % 10];
        const auto& end   = polygon[(i + 1) % 10];

        // Compare point.Y is in [start.Y, end.Y]
        if ((start.y >= point.y) == (end.y >= point.y)) { continue; }

        // Get intersection.
        const float grad = float(end.y - start.y) / float(end.x - start.x);
        const float resX = (point.y + grad * start.x - start.y) / grad;

        if (resX < point.x) { intersected++; }
    }

    return (intersected % 2 != 0) ? true : false;
}
  1. 図形を成す線分を取る。
  2. point点PがY軸に対して線分のY範囲の中にあるかを判断する(条件1)
  3. 線分の勾配(傾き)を求め、線分と半直線が交差している部分の点のY値が同一だと判断し、X値を求む。
  4. X値がpoint点Pより小さいと、交点が点Pより前にあるものだと判断しカウンターを増す。
  5. 最後にカウントが奇数だったらtrueを返す。

したはアルゴリズムを検証するためのテスト図形std::array<DVector, 10>とテスト点P(8, 8)を使用してテストを回したコードです。

struct DVector final { int x, y; };
constexpr std::array<DVector, 10> kMesh =
{
    DVector
    {8, 2}, {11, 10}, {22, 2}, {34, 20}, {26, 14},
    {14, 15}, {27, 18}, {18, 25}, {9, 23}, {4, 17}
};

int main()
{
    constexpr DVector point = {8, 8};
    if (IsInside(kMesh, point) == true) { std::printf("IsInside\n"); }
    else { std::printf("IsNotInside\n"); }

    return 0;
}

点P(8, 8)は上の図によれば図形の内部にあることがわかります。従って、結果もIsInsideと図形の内部にあるという結果を出します。もしある点P'(1, 8)がある場合には、この点は図形の外部にあるためIsNotInsideと結果を出します。

IsInside

参照

  • 図は元記事にあるものを使いました。

https://namu.wiki/w/%EA%B5%90%EC%A0%90

Line–plane intersection - Wikipedia

超簡単なBehaviour Treeを実装してみた

Finite-State Machineとは(Behaviour Treeの以前に。。。)

en.wikipedia.org

https://cdn.tutsplus.com/gamedev/uploads/2013/10/fsm_enemy_brain.png

  • Finite-State Machineは人工知能または条件による行動の分岐を実装する基本的な方法の一つである。
  • 人工知能の各行動をモジュール化させたので、ある程度に柔軟さは持っているが、遷移条件や状態が多くなると設定がやたらに複雑になる可能性がある。
    • 例えば、IDLE MOVE ATTACKという状態ノードがある状態で、さらなるHYPER-ATTACKというノードを追加する時、既存の3つのノードに新しいノードに繊維する条件などを追加しなければならなくなる。(しない場合もあるけど、最悪の場合を考慮)

Behaviour Treeとは

www.gamasutra.com

https://outforafight.files.wordpress.com/2014/07/selector1.png

  • Behaviour TreeはFSMとは違って、ツリーの構造を持っていて、左から右へとトラバーサルをしながら各ノードに合わせて行動を実行をする。
    • FSMの状態遷移の条件は各ノードになる。
  • BTの基本的なノードは、SelectorSequenceがある。(これらをCompositeノードとする)
    • Selectorノードは子ノードの中に一つでもtrueを返すと、残りの子ノードの実行を中断して親ノードに戻る。
    • Sequenceノードは子ノードを全部順番に実行し、もしfalseを返されるまで子ノードを巡回する。
  • Decoratorという種類のノードもあるが、これは一つの子ノードを持って、子ノードのリターン値を加工して親に返す役割をする。
  • BTの根ノードはSequenceノードになる。

下準備

std::string sSpace = "";
struct DSpaceRAII final 
{
  DSpaceRAII() { sSpace += "  "; }
  ~DSpaceRAII() { sSpace = sSpace.substr(0, sSpace.length() - 2); }
};
#define INDENT_SPACE() auto s##__LINE__ = DSpaceRAII{}

各ノードを巡回する時に、std::printfを使用してメッセージを出力します。 その時に、ツリー構造の各レベルにある各ノードのレベルが何なのかを知るため、接頭にレベルごとにスペース文字列を付けます。 下のコードを見たらINDENT_SPACE()が各ノードに付けられていることがわかるでしょう。 自動的にスペースを付けて、そしてスタックが解除されたらスペースを外します。

/// @brief Return random percentage value from 0.0f to 100.0f
float GetRandomPercentage()
{
  static bool isInitialized = false;
  static std::random_device dev{};
  static std::mt19937 rnd{dev()};
  static std::uniform_real_distribution<float> dis{0.0f, 100.0f};

  if (isInitialized == false)
  {
    rnd.seed(static_cast<unsigned>(
      std::chrono::steady_clock::now().time_since_epoch().count()));
    isInitialized = true;
  }
  return dis(rnd);
}

各ノードの分岐によるtrueまたはfalse値の返しを実装するために、0から100までの確率を返す関数を実装します。 返す値に対しての確率は全部同じです。std::uniform_real_distribution<>

class IBehaviourNode
{
public:
  virtual ~IBehaviourNode() = 0;
  virtual bool Invoke() = 0;
};
inline IBehaviourNode::~IBehaviourNode() = default;

class ACompositeNode : public IBehaviourNode
{
public:
  virtual ~ACompositeNode() = 0;

  template <typename TType, typename... TArgs>
  TType& AddChild(TArgs&&... args)
  {
    static_assert(std::is_base_of_v<IBehaviourNode, TType>);
    auto& ref = this->mChildren.emplace_back(std::make_unique<TType>(std::forward<TArgs>(args)...));
    assert(ref != nullptr);

    return static_cast<TType&>(*ref);
  }

  const auto& GetChildren() { return this->mChildren; }

private:
  std::vector<std::unique_ptr<IBehaviourNode>> mChildren; 
};
inline ACompositeNode::~ACompositeNode() = default;
  • ACompositeNodeSequenceSelectorを実装するための抽象クラスです。子ノード(IBehaviourNodeを継承したタイプ)を持つことができます。
  • 一般のBTノードはIBehaviourNodeを継承して実装します。
class FSelector final : public ACompositeNode
{
public:
  virtual ~FSelector() = default;
  bool Invoke() override final
  {
    INDENT_SPACE();
    std::printf("%sFSelector Invoked!\n", sSpace.c_str()); 
    for (auto& node : this->GetChildren()) { if (node->Invoke() == true) { return true; } }
    return false;
  }
};

class FSequence : public ACompositeNode
{
public:
  virtual ~FSequence() = default;
  bool Invoke() override 
  {
    INDENT_SPACE();
    std::printf("%sFSequence Invoked!\n", sSpace.c_str()); 
    for (auto& node : this->GetChildren()) { if (node->Invoke() == false) { return false; } }
    return true;
  }
};

class FRoot : public FSequence
{
public:
  virtual ~FRoot() = default;
  bool Invoke() override final
  {
    std::printf("%sFRoot Invoked!\n", sSpace.c_str()); 
    for (auto& node : this->GetChildren()) { if (node->Invoke() == false) { return false; } }
    return true;
  }
};

SequenceSelector、そして根ノードRootを実装しました。根ノードはSequenceを継承してます。

class FCondition final : public IBehaviourNode
{
public:
  virtual ~FCondition() = default;
  bool Invoke() override final
  {
    INDENT_SPACE();
    if (GetRandomPercentage() < 50.f)
    {
      std::printf("%sFCondition Invoked! => true\n", sSpace.c_str()); 
      return true;
    }

    std::printf("%sFCondition Invoked! => false\n", sSpace.c_str()); 
    return false;
  }
};

class FAction final : public IBehaviourNode
{
public:
  virtual ~FAction() = default;
  bool Invoke() override final 
  { 
    INDENT_SPACE();
    std::printf("%sFActor Invoked! => true\n", sSpace.c_str()); 
    return true; 
  }
};

class FMove final : public IBehaviourNode
{
public:
  virtual ~FMove() = default;
  bool Invoke() override final 
  { 
    INDENT_SPACE();
    std::printf("%sFMoved Invoked! => true\n", sSpace.c_str()); 
    return true; 
  }
};

class FCondition2 final : public IBehaviourNode
{
public:
  virtual ~FCondition2() = default;
  bool Invoke() override final
  {
    INDENT_SPACE();
    if (GetRandomPercentage() < 50.f) 
    { 
      std::printf("%sFCondition2 Invoked! => true\n", sSpace.c_str()); 
      return true; 
    }

    std::printf("%sFCondition2 Invoked! => false\n", sSpace.c_str()); 
    return false;
  }
};

一般ノードを実装しました。そして下のコードでBehaviour Treeを組みます。

int main()
{
  auto root = std::make_unique<FRoot>();
    auto& selector = root->AddChild<FSelector>();
      auto& seq0 = selector.AddChild<FSequence>();
        seq0.AddChild<FCondition>();
        seq0.AddChild<FAction>();
      auto& seq1 = selector.AddChild<FSequence>();
        seq1.AddChild<FMove>();
    root->AddChild<FCondition2>();

  for (int i = 0; i < 10; ++i)
  {
    const auto result = root->Invoke();
    std::printf("[%2d] Result : %s\n\n", i, result ? "true" : "false");
  }

  return 0;
}

実行すると、以下の様にBehaviour Treeが実行します。 各ノードはそれぞれのロジックを実行して、truefalseを親ノードに返すと、親ノードは親ノードの種類によって分岐をしてくれます。

FRoot Invoked!
  FSelector Invoked!
    FSequence Invoked!
      FCondition Invoked! => true
      FActor Invoked! => true
  FCondition2 Invoked! => true
[ 0] Result : true

FRoot Invoked!
  FSelector Invoked!
    FSequence Invoked!
      FCondition Invoked! => true
      FActor Invoked! => true
  FCondition2 Invoked! => false
[ 1] Result : false

FRoot Invoked!
  FSelector Invoked!
    FSequence Invoked!
      FCondition Invoked! => true
      FActor Invoked! => true
  FCondition2 Invoked! => true
[ 2] Result : true

FRoot Invoked!
  FSelector Invoked!
    FSequence Invoked!
      FCondition Invoked! => false
    FSequence Invoked!
      FMoved Invoked! => true
  FCondition2 Invoked! => false
[ 3] Result : false
  • FSMとは違って、BTは条件チェックが自分のノードとそしてノードに付けられている1階層下の子ノードだけとのことになるので、複雑にならなく、拡張が自由である。
  • そして根ノードがSequenceであるため、他のBTに子ノードとして付けても構わない。

ということで、超簡単なBTを実装してみました。実際に実装してみたら思ってたのより簡単でした。

UE4のBTとの違い

  • UE4で実装しているBTは、基本的なBTとはちょっと違う。
    • BlackBoard(ブラックボード)という、BTで変数の値を共有するために実装されたコンテナがある。
    • Service(サービス)という、コンポジットノード下の子ノードが実行している間に指定したフレームごとに別の処理を行うことが出来る機能がある。主にBlackBoardの値を書き換える時に使われるそうだ。
    • Decoratorはコンポジットまたは一般ノードにつけられるし、そしてDecorator自体が条件式を持ち、結果によって子ノードを実行するか判断もできる。
    • Simple Parallelという、サブツリーと共に並列にノードのロジックを実行できるようにしたコンポジットノードもある。この並列ノードの終わり方もさまざま。

自分で実装してみたいんですけど長道になりそうですねー

参照

kindtis.tistory.com

e-words.jp

基礎から学ぶ!UE4でC++を交えたAI開発、興味ないですか? - Speaker Deck

「8 Most Common 3D File Formats in 2019」のメモ

all3dp.com

  • 3Dファイルの基本的な目的は、3D情報をテキストまたは特定のテンプレートを持ったバイナリファイルに込めたものである。
    • 3D情報とは、Geometry, Appereance, SceneそしてAnimationsがある。3Dモデルのファイルの中では一部しか込められないものもある(例えばSTL)
    • GeometryはModelの図形の情報を持つ。
    • AppearanceはModelの色、テクスチャの情報、マテリアルのタイプなどがある。
    • Sceneは3Dモデル以外に光の位置、カメラ、また補助のオブジェクトの情報を持つ。
    • Animationは3Dモデルのアニメーションを記す。

Neutralな3Dモデルのファイルが一番

  • それぞれの3Dツールからの独占的なファイル以外に、オープンソースか独占的でない3Dファイルもある。代表的な例がSTLCOLLADA。この2つのファイルは主にCADで使われる。
3D File Format Type
STL 独立
OBJ ASCII形式のファイルだけ独立
FBX 独占的だが、広く使われている
COLLADA 独立
3DS 独占
IGES 独占
STEP 独占
VRML/X3D 独占
glTF 独立

どれを使うか

  • 3Dプリンティングをさせる時には、プリンター自体の解像度に限界があって、そして一つの色しか使うことが多いので大体はSTLを使うらしい。STLはGeometryの情報しかセーブ出来ないようだ。
    • OBJの場合にはSTLにAppearanceの情報までセーブすることが出来る。
  • ゲームかレンダリングさせる場合には、アニメーションとAppearanceが豊かじゃないとならないため、FBXまたはCOLLADAが主流である。(glTFというのも全て支援しているが、Faceが三角形に限られるというのが短所)
  • IGESSTEPはエンジニアリング(飛行機など)に使われるそうだ。ものすごく精密なのが長所らしい。

各フォーマットの特徴

OBJ

  • ジオメトリを生成する時にApproximateかPreciseか選択することが出来る。Approximateの場合、Faceが三角形に限らなくても良い。Preciseの場合には表現がNURBSを使用する。(NURBSB-Splineの変種)
  • Appearanceの支援をしている。しかし別途のファイル(.MTL)として情報をセーブしているし、.OBJではその.MTLのパスをしてしているだけである。
  • ASCIIの.OBJファイルはオープンソースである。

FBX

en.wikipedia.org

  • AutoDeskからの独占的なフォーマットで、今は多くのゲームか映画業界などで使われている。
  • ジオメトリ、見た目、そしてスケルトンアニメーション、Morphsまで支援している。
  • バイナリとASCII両方支援している。

COLLADA

  • .FBXとは違って、Khronos Groupから管理している中立(独立)性のフォーマットである。
  • .FBXが支援している機能は全て支援しているし、物理に関する値もセーブ出来る。
  • XMLマークアップ言語を使用して値をセーブしている。
  • しかし.FBXよりは人気度が低い。

参照

f:id:neuliliilli:20190517214834p:plain
8個のフォーマットの比較表

「ゲームプログラマの実力向上法」のメモ

www.slideshare.net

  • 「コンシューマ>オンライン>モバイル>順でプラットフォームによって実力が優れているわけでもない。
    • 昔はそうだったが、UnityかUE4のエンジンが登場し、モバイルに写った開発者が多くなるにつれてそういう認識が破れていった
    • しかしデスクトップかスマートフォンかローレベルコーディングから開発してみれば本当に有利になる。
  • 経歴が多いことと、その人が持っている実力が比例しているわけでもない。
  • 話すことが上手としても必ず実力が優れているわけでもない。しかしその逆は大体通じる。実力があるプログラマはちゃんと話そうと努力しているそう。

筆者の主観的な実力向上法

  1. 頭またはツールを利用して目的スケージュール、そして実行タスクを一つずつ実践していく。
  2. プログラミングのいい習慣について悩み続ける。
  3. 他のプログラマさんをメンターにして栄養がある質問をしつこく(?)する。
  4. 実力が良い会社に転職する。
  5. 会社でゲームを開発しながらR&D(Research&Development)をする。

  1. オフィスのデスクにゲーム開発に必要となる本を持ってきて、さり気なくその場で読む。
  2. 週末に通勤しないと、勉強会に参加する。
  3. シニアになってもポートフォリオはアップデートしてGithubなどにアップロードし続く。
  4. 新しく習った技術が現在の会社に適用することが出来なかったら、個人作でも作って適用してみる。
  5. ウェブ上の記事は大体英語でなっている為、できればプリンターで出力してみる。

  1. ゲーム開発のセミナーに参加する。
  2. ゲーム開発に携わっている先輩たち、開発者さんと一緒に親しくなる。
  3. 一週に一度はオフラインまたはオンライン書店によって技術のトレンドを把握する。
  4. 読んでみた本は必ずまとめてブログかSlideShareに投稿。

実力の向上グラフ・T字型人材

  • 実力の向上は勉強する量に対して正比例するのではなく、一定時間渋滞の状態でいきなり一段階跳ね上がる。
    • ずーっと勉強し続くこと。
  • T字型人材は
    • 様々な方面に水準が高い技術を持ち(Generallist)
    • そしてその中で一つの分野では最高となる技術を持つ(Specialist)。