超簡単な炎シェーダーを実装してみた。
今更ですが、「Shade」というiOSのシェーダー作成アプリケーションを使用して超簡単な炎を作ってみました。「Shade」というアプリケーションにはHLSLまたはGLSLのdiscard
とか、lerp
(mix
)という関数の機能を提供してくれるノードがないため、ちょっと余計に複雑になりました。ですけどノイズマップとグラデーションを使用して類似乱数みたいな表現が出来たという基本的なテクニックは様々なところに使えそうだから良いですね。
Shadeを使用した実装
ノイズマップを使用した炎は以下の順番で実装が出来ます。
- ノイズマップと、グラデーションマップを準備。炎が上に燃え立つよう、ノイズモップのUVの
V
部分は上へ移動するように。 - ノイズマップを基準として(
y
)、step
関数を行う。値が0
か1
へと分かれる。 - 色を付けることで一番簡単な炎が出来上がる。
- ノイズマップの値を上げることで、
step
以後の炎の分布はどんどん下へとさがる。これを利用して段階的に色をつけて、lerp
(線形補間)かsmoothstep
で炎にグラデーションをつけることも可能。 - 炎ではない部分は
discard
で着色を取り消す。(Shadeアプリではdiscard
がない為、単に黒い色に。)
具体的な実装手順
スクリーンスペースのScaledされた座標をUVとして扱います。しかしノイズマップを上へ移動させるため、Time
ノードから(秒単位で値を返す)少数部分を取り出し、V
へ足すことでノイズマップを縦移動させます。
Step
を使用してノイズの一部分を取り、そして引き算をさせることで赤、オレンジ、そして黄色になる部分を取り出します。Subtract
One Minus
Subtract
順に赤色、黄色、オレンジ色の配置となります。
線形補間を使用して色を付けます。まずは、赤色と黄色を付けてから、そして混合したものをまたオレンジ色と線形補間をして最終的に混合させます。
Shadeアプリケーションではdiscard
ノードがないため、炎ではない部分をただの黒に塗り替えてから色を表示させます。
これで3段階で色がある炎が実装できました。もっと複雑な模様で炎を出すためには、マスクマップを使用してマクスされていない部分を取り除けば良くなりそうと思います。
「2D多角形の内部、外部判定のやり方」のメモ
判定とは一体
- 2Dの多角形に対し、AまたはBみたいな点が図形の内部にあるか外部なのかを判別することを示す。
図形の内部に位置する点の特徴は
- 図形の外部から点を通過するRay(光線)を放って、点Pに到達するまでの図形の交点の数を求めば、その点が図形の内部にあるかないかが分かる。
- 簡単ながらもすごいアルゴリズム。ちょっと変形すれば3Dの三角形トポロジーのメッシュにも適用が出来る。
こっちでは上の図とは違って、左から右へと放つRayを使用して交点を求めます。だとしても外部にある点は偶数で、内部にある点は奇数ということは変わりません。
実装
2Dの場合、ある点Pが図形の中にあるかを判断するために放つRayはXかY軸に並行したものとすれば実装が簡単になります。(3Dはちょっと複雑になります)
- Rayのスターティングポイントから点Pまでの直線を「半直線」という。
- 図形を成す全ての線分と、
- 半直線が「交差」して、
- 点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; }
- 図形を成す線分を取る。
point
点PがY軸に対して線分のY範囲の中にあるかを判断する(条件1)- 線分の勾配(傾き)を求め、線分と半直線が交差している部分の点のY値が同一だと判断し、X値を求む。
- X値が
point
点Pより小さいと、交点が点Pより前にあるものだと判断しカウンターを増す。 - 最後にカウントが奇数だったら
true
を返す。
したはアルゴリズムを検証するためのテスト図形std::array<DVector, 10>
とテスト点Pを使用してテストを回したコードです。
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は上の図によれば図形の内部にあることがわかります。従って、結果もIsInside
と図形の内部にあるという結果を出します。もしある点P'がある場合には、この点は図形の外部にあるためIsNotInside
と結果を出します。
IsInside
参照
- 図は元記事にあるものを使いました。
超簡単なBehaviour Treeを実装してみた
Finite-State Machineとは(Behaviour Treeの以前に。。。)
- Finite-State Machineは人工知能または条件による行動の分岐を実装する基本的な方法の一つである。
- 人工知能の各行動をモジュール化させたので、ある程度に柔軟さは持っているが、遷移条件や状態が多くなると設定がやたらに複雑になる可能性がある。
- 例えば、
IDLE
MOVE
ATTACK
という状態ノードがある状態で、さらなるHYPER-ATTACK
というノードを追加する時、既存の3つのノードに新しいノードに繊維する条件などを追加しなければならなくなる。(しない場合もあるけど、最悪の場合を考慮)
- 例えば、
Behaviour Treeとは
- Behaviour TreeはFSMとは違って、ツリーの構造を持っていて、左から右へとトラバーサルをしながら各ノードに合わせて行動を実行をする。
- FSMの状態遷移の条件は各ノードになる。
- BTの基本的なノードは、
Selector
とSequence
がある。(これらを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;
ACompositeNode
はSequence
とSelector
を実装するための抽象クラスです。子ノード(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; } };
Sequence
とSelector
、そして根ノード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が実行します。
各ノードはそれぞれのロジックを実行して、true
かfalse
を親ノードに返すと、親ノードは親ノードの種類によって分岐をしてくれます。
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
という、サブツリーと共に並列にノードのロジックを実行できるようにしたコンポジットノードもある。この並列ノードの終わり方もさまざま。
自分で実装してみたいんですけど長道になりそうですねー
参照
「8 Most Common 3D File Formats in 2019」のメモ
- 3Dファイルの基本的な目的は、3D情報をテキストまたは特定のテンプレートを持ったバイナリファイルに込めたものである。
- 3D情報とは、Geometry, Appereance, SceneそしてAnimationsがある。3Dモデルのファイルの中では一部しか込められないものもある(例えばSTL)
- GeometryはModelの図形の情報を持つ。
- AppearanceはModelの色、テクスチャの情報、マテリアルのタイプなどがある。
- Sceneは3Dモデル以外に光の位置、カメラ、また補助のオブジェクトの情報を持つ。
- Animationは3Dモデルのアニメーションを記す。
Neutralな3Dモデルのファイルが一番
- それぞれの3Dツールからの独占的なファイル以外に、オープンソースか独占的でない3Dファイルもある。代表的な例が
STL
とCOLLADA
。この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が三角形に限られるというのが短所) IGES
かSTEP
はエンジニアリング(飛行機など)に使われるそうだ。ものすごく精密なのが長所らしい。
各フォーマットの特徴
OBJ
- ジオメトリを生成する時にApproximateかPreciseか選択することが出来る。Approximateの場合、Faceが三角形に限らなくても良い。Preciseの場合には表現が
NURBS
を使用する。(NURBS
はB-Spline
の変種) - Appearanceの支援をしている。しかし別途のファイル(
.MTL
)として情報をセーブしているし、.OBJ
ではその.MTL
のパスをしてしているだけである。 - ASCIIの
.OBJ
ファイルはオープンソースである。
FBX
- AutoDeskからの独占的なフォーマットで、今は多くのゲームか映画業界などで使われている。
- ジオメトリ、見た目、そしてスケルトンアニメーション、Morphsまで支援している。
- バイナリとASCII両方支援している。
COLLADA
.FBX
とは違って、Khronos Groupから管理している中立(独立)性のフォーマットである。.FBX
が支援している機能は全て支援しているし、物理に関する値もセーブ出来る。- XMLマークアップ言語を使用して値をセーブしている。
- しかし
.FBX
よりは人気度が低い。
参照
「ゲームプログラマの実力向上法」のメモ
www.slideshare.net
- 「コンシューマ>オンライン>モバイル>順でプラットフォームによって実力が優れているわけでもない。
- 昔はそうだったが、UnityかUE4のエンジンが登場し、モバイルに写った開発者が多くなるにつれてそういう認識が破れていった
- しかしデスクトップかスマートフォンかローレベルコーディングから開発してみれば本当に有利になる。
- 経歴が多いことと、その人が持っている実力が比例しているわけでもない。
- 話すことが上手としても必ず実力が優れているわけでもない。しかしその逆は大体通じる。実力があるプログラマはちゃんと話そうと努力しているそう。
筆者の主観的な実力向上法
- 頭またはツールを利用して目的とスケージュール、そして実行タスクを一つずつ実践していく。
- プログラミングのいい習慣について悩み続ける。
- 他のプログラマさんをメンターにして栄養がある質問をしつこく(?)する。
- 実力が良い会社に転職する。
- 会社でゲームを開発しながらR&D(Research&Development)をする。
- オフィスのデスクにゲーム開発に必要となる本を持ってきて、さり気なくその場で読む。
- 週末に通勤しないと、勉強会に参加する。
- シニアになってもポートフォリオはアップデートしてGithubなどにアップロードし続く。
- 新しく習った技術が現在の会社に適用することが出来なかったら、個人作でも作って適用してみる。
- ウェブ上の記事は大体英語でなっている為、できればプリンターで出力してみる。
- ゲーム開発のセミナーに参加する。
- ゲーム開発に携わっている先輩たち、開発者さんと一緒に親しくなる。
- 一週に一度はオフラインまたはオンライン書店によって技術のトレンドを把握する。
- 読んでみた本は必ずまとめてブログかSlideShareに投稿。
実力の向上グラフ・T字型人材
- 実力の向上は勉強する量に対して正比例するのではなく、一定時間渋滞の状態でいきなり一段階跳ね上がる。
- ずーっと勉強し続くこと。
- T字型人材は
- 様々な方面に水準が高い技術を持ち(Generallist)
- そしてその中で一つの分野では最高となる技術を持つ(Specialist)。