Coder Social home page Coder Social logo

luminoengine / lumino Goto Github PK

View Code? Open in Web Editor NEW
206.0 206.0 14.0 123.23 MB

Lumino is a framework for building real-time graphics applications.

Home Page: https://luminoengine.github.io

License: MIT License

C++ 95.20% C 1.95% CMake 0.26% C# 0.33% JavaScript 0.03% Objective-C++ 0.02% GLSL 0.01% HLSL 0.75% Objective-C 0.01% Shell 0.01% Ruby 0.33% Batchfile 0.01% QMake 0.01% QML 0.01% Python 0.01% Vim Snippet 0.01% NASL 0.01% ActionScript 0.22% Haskell 0.87% HTML 0.01%
c-plus-plus directx-12 game-engine graphics-engine multi-platform ruby vulkan

lumino's People

Contributors

dependabot[bot] avatar infinnie avatar lriki avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

lumino's Issues

Mesh クラスの頂点フォーマットを汎用的にする

Proposal

MeshResource は内部に合計 5 つの VertexBuffer を持っているが、フォーマットが MMD モデルに寄っているため、汎用的にしたい。

enum VertexBufferGroup
{
	VBG_Basic = 0,
	VBG_BlendWeights = 1,
	VBG_AdditionalUVs = 2,
	VBG_SdefInfo = 3,
	VBG_MmdExtra = 4,
};

また、VBG_Basic の頂点フォーマットは次のようになっている。

struct Vertex
{
	Vector3 position;
	Vector3 normal;
	Vector2 uv;
	Color color;
};

汎用化を目指すなら Unity や Three.js のように、各要素で別々の配列 (頂点バッファ) を指定できるようにした方が良いのでは?

var geometry = new BufferGeometry();
geometry.addAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
geometry.addAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );

Motivation

  • フォーマットが MMD モデルに寄っているため、汎用的にしたい。
  • 自由に頂点データを追加したい。
  • glTF のように非常に柔軟な頂点フォーマットを持つモデルの読み込みに対応したい。

Implementation

VertexLayout 付きの頂点データを外部から指定できるようにする、という方針となる。

内部データは VertexBuffer が分かれても、ひとつにまとめても構わない。ただ、内部フォーマットへの変換処理を実装することになる。

要素ごとに VertexBuffer を分けるべきか?

編集が簡単になるが、描画時のキャッシュ効率が落ちる。

編集と言っても、そもそも一般的に描画用メッシュクラスは編集用に作られてはおらず、編集したければ Half-Edge Mesh などを使うべき。
頂点位置の微調整や法線計算、箱や球といった他のメッシュを(描画効率向上のため)マージするのはサポートしてもいいともうけど…。

各言語バインダ用の API

例えば、Ruby は構造体という仕組みが無いので、VertexBuffer にバイナリデータを直書きするのか、アクセサを提供するのか。

バイナリデータを直接扱うのは言語によってかなり得手不得手あるので、基本はアクセサの提供にしたい。

ちなみに UE4 は、各要素の配列を受け取って Mesh を作るユーティリティがある。
https://docs.unrealengine.com/en-US/API/Plugins/ProceduralMeshComponent/UProceduralMeshComponent/CreateMeshSectio-/index.html

Research

UE4

StaticMesh の頂点バッファは次のようになっている。

struct FStaticMeshVertexBuffers
{
	FStaticMeshVertexBuffer StaticMeshVertexBuffer;	// Normal, Tangent, Binormal, UV
	FPositionVertexBuffer PositionVertexBuffer;		// Pos のみ
	FColorVertexBuffer ColorVertexBuffer;			// Color のみ
}

やはりある程度の要素はひとつの VertexBuffer にまとめることで、キャッシュの効率化を狙っている?

Physics モジュールの API 整理

Unity などの PhysX 系と Bullet 系ではこの辺の誰がどんな情報を持つのかが結構違うので注意。
ざっくり以下のような感じ。

PhysX

  • Body : 姿勢、質量など
  • Material : 摩擦、弾性
  • Collider : 形状、ローカル姿勢

Bullet

  • Body : 姿勢、質量、摩擦、弾性など
  • Collider(Shape) : 形状 ※ローカル姿勢はbtCompoundShape を通して制御

Lumino としては

内部的には Bullet だけど、API は Unity 的に持っていく。
なぜかというと、コンポーネント指向でオブジェクトを構築する場合 Bullet そのままよりも都合がいいから。
特に、Collider にローカルオフセットをプロパティとして持たせることでエディタ操作やシリアライズが楽になる。
(Bullet のままだと追加される側、つまり Body 側の各子Shapeに付属されるプロパティとなってしまう。面倒)

あと、CollisionShape という名前を Collider にする。

使ってみたい

はじめまして、ギバロウと申します。
しがないHSP3ユーザーです。
最近になってLuminoを知り、HSP3でこんなにも高度な3D表現が可能なことに感動しました。

自分でも使わせていただきたいと思い、リファレンスを見ながら試行錯誤したのですが、自作のモデルすら表示させることが出来ませんでした。

もし可能でしたら、自作モデルの読み込みから表示、また、アニメーション付きモデルの読み込み表示や、モーションの変化等、ご教授いただけたらと思い、こちらに書き込みさせていただきました。

よろしくお願いします。

法線マップをサポートする

Proposal

法線マップテクスチャは Material に設定して使う。

material->setNormalMap(Texture2D::load(u"normal.png"));

接線ベクトルは標準頂点データとして追加する。

 struct Vertex
 {
     Vector3 position;
     Vector3 normal;
     Vector2 uv;
     Color color;
+    Vector4 tangent;
 };

VertexTangents は削除。

シェーダについては、法線マップ処理の有無をスイッチできるようにする。

 @module
 techniques:
     Forward_Geometry_UnLighting:
         passes:
         -   Pass0:
+                defines: USE_NORMALMAP
                 vertexShader: VS_ClusteredForward_Geometry
                 pixelShader: PS_Main
 @end

※define よりは TechniqueClass に新しいグループを作ったほうがいいかも。

Implementation

Unity や three.js と同じにしてみる。

従法線ベクトルは頂点シェーダで計算する。現状頂点セマンティクスとして Binormal はサポートしているが、Tangent だけにしてみる。

three.js の実装メモは後述。

Note

懸念 - struct Vertex のデータサイズ増大

そろそろ無視できなくなってきそう。

これまでの想定としては従法線も CPU 側で計算する予定だったから VertexTangents を作って、標準頂点とは別ストリームの頂点バッファとして作る予定だった。

VertexTangents はこんな感じ。

struct VertexTangents
{
    Vector3 tangent;
    Vector3 binormal;
};

結果的に Vector4 だけで済んだので別頂点バッファにするのは大げさかな…ということで標準頂点の方に統合してみる。

実際に Lumino を使ったシーンでどれだけ法線マップが使われるかは何とも言えないけど…。

これ以上別なデータが増えそうならまた分離を考えよう。

ちなみに統合しない場合、MeshGenerator で作っている球とかの標準形状で複数の頂点バッファが必要となり、かなり大掛かりな変更が必要になる。

シェーダ

法線テクスチャを使わないときは、青で塗りつぶしたデフォルトのテクスチャを使用すればスイッチの必要はないが、
シェーダだけでなく CPU 側でも ModelView の逆行列を計算したりと、それなりの負荷の処理を行うため切り替えられるようにしておく。

three.js

defaultnormal_vertex.glsl.js で、やっぱりView空間へ変換しているようだ。

vec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;

次に normal_vert.glsl.js で

vTangent = normalize( transformedTangent );
vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );

tangent は vec4。.w は求めた 従法線 を反転するかどうかを示す、おそらく 1 or -1 の値が入っている。

その他

  • normal_fragment_begin.glsl.js
  • normal_fragment_maps.glsl.js

Unity

three.js と同じ実装のようだ。

UIレイアウトシステム再設計

flex だけ、はやめよう

ui-layout-2 ブランチで yoga を使った flex レイアウトをデフォルトにしてみたが、どうにも flex レイアウトはゲームでは使いづらい。
画面の上下左右にスナップしたコンテナをまずルート要素近くに用意し、flex はその中で使うことが多いだろう。(それでもほとんど StackPanel で足りるが)

grid layout は欲しい。この grid も、WPF のような高さを指定できるものと Flutter のような列数だけ指定してあとは Flow にするもの、など欲しい。

Style vs 派生クラス

そうするとやっぱり UIElement ごとにレイアウト方法を変えたくなる。

HTML/CSS のように Style でやるならこんな感じ。

auto i = UIElement::With()
    .layoutDisplay(UILayoutDisplay::Grid)
    .size(100, 100)
    .build();

これまでのように派生クラスを用意するならこんな感じ。

auto i = UIGridLayout::With()
    .size(100, 100)
    .build();

後者の方が読みやすいだろう。

WPF, UE, Godot, Kivy, Flutter, Android あたりは後者。Unity の UIElements は前者。ゲームエンジンでは前者は珍しいかも。

デフォルトの Layout は?

これまで同様でよいだろう。
ただ、UIVAlignment, UIHAlignment をやめたい。この指定がどうしても冗長になってしまう。

次のようにできないだろうか?

  • Margin の有無で Alignment を決める。
    • Left または Right が指定されていたら、UIHAlignment::Left, UIHAlignment::Right と同義。
    • 両方指定されていたら、UIHAlignment::Sretch と同義。
    • 両方指定されていなかったら、UIHAlignment::Center と同義。
    • Sretch 以外の場合、幅は Width を使う。Width 未指定の場合、desirdSize を使う。

パーセントの指定方法はどうしよう?

Litho は positionDippositionPercent といった具合で別関数がある。というかコアになってる yoga そのまま。
https://fblitho.com/javadoc/com/facebook/litho/Component.Builder.html

ComponentKit は オブジェクトを渡す。
https://componentkit.org/docs/avoid-width-100-percent/

Flutter, Kivy, WPF はそもそも無いみたい。親の Grid など側で割合を指定できる。
https://stackoverflow.com/questions/43122113/sizing-elements-to-percentage-of-screen-width-height

Grid があればパーセントの指定は要らないかなという気がする。そうすると nan を無効値として扱うだけでよいため API はかなりシンプルにできる。

C++ 版 DLL

動機

現状は StaticLib としての配布のみであるが、サイズが非常に大きくなっている。

対策として DLL 配布できないか検討したい。

技術的課題

シンボルのエクスポート

外部に公開するクラスや関数を __declspec(dllexport) 等で修飾する必要がある。
コンパイラの差を吸収するため LN_API マクロが定義されているので、これを使う。

ビルド時とリンク時で同じバージョンのコンパイラが必要

これは StaticLib と同じ運用となる。

STL の制約

エクスポートするクラスは、ベースクラスやメンバ変数に STL のクラスを持ってはならない。

自分でコンテナクラスを作成したとき、そのクラスが std::vector を継承していたり、メンバ変数に内包していたりするとアウト。(VisualC++ で警告が出てくる)
あるいは PImpl の実装にする必要がある。

2021/2/4 時点の方針

上記の通り STL を使っている個所を何とかしないと進めることはできない。

ひとまず見送り。

MeshModel 再設計

Proposal

Instantiate

WorldObject を作るときは次のようにできるようにする。

auto model = MeshModel::load(u"model.gltf");
auto obj1 = StaticMesh::create(model);
auto obj2 = SkinnedMesh::create(model);

現行↓

auto model1 = StaticMeshModel::load(u"model.gltf");
auto model2 = SkinnedMeshModel::load(u"model.gltf");
auto obj1 = StaticMesh::create(model1);
auto obj2 = SkinnedMesh::create(model2);

動的データの操作

WorldObject(Component) からアクセスする。

auto obj2 = SkinnedMesh::create(model);

// フォーマルな方法
SkinnedMeshBone* bone = obj2->skinnedMeshComponent()->findBone(u"");
bone->setRotation(...);

// ユーティリティ
SkinnedMeshBone* bone = obj2->findBone(u"");
bone->setRotation(...);

SkinnedMeshBone は Transform の派生とし、Editor 上からアクセスできるようにする。 (Unity と同じ仕組み)

Motivation

データの分離

MeshModel(StaticMeshModel と SkinnedMeshModel) は静的なデータと動的なデータが統合されていて、リソースの共有が難しい。
これを分離したい。

静的なデータは次の通り。

  • Mesh(LOD)
    • VertexBuffer
    • IndexBuffer
    • Section
    • モーフィング用位置VertexBuffer
  • Material
  • Node構造
  • InitialTransform
  • Bone構造
  • IK情報

動的なデータは次の通り。

  • AnimationController
  • SkinningMatrices(Bone姿勢)
  • SkinningMatricesTexture(SkinningMatrices を GPU に渡すバッファ)
  • Morph ブレンド率

継承関係の廃止

SkinnedMeshModel は StaticMeshModel の派生クラスとなっているが、
例えば .gltf をインポートしただけではどちらのインスタンスを作るべきか判断できないことがある。
そのため MeshModel クラスに統合し、Bone の有無などはコンポーネント志向の考え方で持たせたい。

Note

Model:Instance の対応関係は次のようなイメージ

  • MeshModel(*) - MeshModelInstance(今のところ公開予定なし)
  • MeshBoneModel - SkinnedMeshBone(*)

*: ユーザープログラムに積極的に公開することになるクラス。

Example は MainLoop ベースにするべきか?

しない。

以下、検討結果。

動機

Lumino は次の2つのエントリーポイントの作成方法がある。

  • Application クラスを継承して実装する
  • Main() に while 等を使ってメインループを実装する

Application の実装では仮想関数などの知識が必要となるため、メインループと比べてやや前提知識のレベルが上がる。
簡単なサンプルではメインループを使って実装した方がわかりやすいのでは?

特に C と HSP3 へ対応するにあたって、継承といった概念の無い言語向けに作るサンプルが少し難しくならないか心配。

対案

なぜ Application クラスを使うのか?

メインループの隠蔽が必要なケースに対応するため。

Web や Android 環境で期待通りのメインループを実装することは困難で、どうしても使いたいときはサブスレッドを使う必要がある。
ただサブスレッドでは UI 関係の API に制約がかかることが一般的で、問題の原因となりやすい。

また update のタイミングをエンジン側で完全に制御できるようになるので、
一時停止、ヒットストップ(これは Application でやるべきではないかもしれないが) といった時間制御がやり易くなったり、
メッセージキューが空なら更新しないことで負荷を落とすといった制御をすることが可能になる。

オブジェクト指向の知識を前提とする懸念はあるが、アプリの規模が大きくなりシーン遷移が必要になってくると、
どのみち同じように継承&仮想関数(or コールバック登録)の実装が必要になったり、この辺りの知識が必須になってくる。

なぜメインループをサポートするのか?

Moduler Engine として、他のフレームワークに組み込めるようにするため。

ただこの場合、FPS制御などは他フレームワーク側に任せることになるため Engine::update はやめて、FrameWindow の描画関数や更新関数を直接呼び出すようにした方がいいかもしれない。

逆にこのFPS制御やシステムのメッセージキューの処理を誰に任せるかはっきりさせない限り、メインループサポートは待った方がよさそう。
(例えば HSP3 で await は誰が呼び出せばいいの?とか)

Research

ざっと集めてみたゲームエンジン・ライブラリでは、Application クラス相当のエントリーポイントとなっているものが圧倒的に多かった。

メインループをサポートしているのは、低レイヤーの描画ライブラリであることが多い感じ。

Application クラス相当

Unity など大きなゲームエンジンや、Web系などプラットフォームの制約がかかるものは全てこちら。

Amethyst(rust)

Piston(rust)

Panda3D(python)

pyxel(python)

pyglet(python)

kivy(python)

KivEnt(python)

Tkinter(python)

defold(lua)

love2d(lua)

Gosu(ruby)

Shoes(ruby)

Ruby2D(ruby)

DotFeather(C#)

Stride(C#)

Ogre(C++)

openFramework

Cinder

Pomdog(C++)

MEGA(C++)

Processing

MainLoop

SDL

Altseed

Siv3D

DxRuby

pygame(python)

rubygame(ruby)

シェーダに複数 technique が書かれている場合、最初の technique 以外で使用している uniform 変数に値が設定されない

例えば↓のようにしているとき、ln_View について VS_ClusteredForward_Geometry では使わず、VSI_Main で使っているとき、ln_View がall 0 になってしまう。

technique Forward_Geometry
{
    pass Pass1
    {
        VertexShader = VS_ClusteredForward_Geometry;
        PixelShader = PS_Main;
    }
}

technique Forward_Geometry_UnLighting_Instancing
{
    pass Pass1
    {
        VertexShader = VSI_Main;
        PixelShader = PSI_Main;
    }
}

Camera の実装を PerspectiveCamera と OrthographicCamera に分ける

near プロパティの役割が異なるため。

PerspectiveCamera の near としては、0.0 は無効。ViewFrustum を作成できない。(一部の面情報が nan になる)

OrthographicCamera の near としては、0.0 は有効。Sprite2D のデフォルト Z は 0.0 であり、これより大きい場合デフォルトで表示されなくなってしまう。一般的にも期待される初期値だろう。

また、PerspectiveCamera は fovY、アスペクト比が必須。一方、OrthographicCamera はビューポートに投影するワールドの範囲を示す width, height が必須。
これらでコンストラクタを作ると、似たような型が並んでしまうのであいまいさのリスクが出てくる。

help needed

I'd like to use Lumino unfortunately the GitHub documentation is not in English and didn't succeed to understand.
Do you plan an English translation of your Github repository.
thanks

FileSystem のエラーハンドリングを見直し

そもそも FileSystem が必要なのか?

Lumino の FileSystem の目的の半分は、アプリ開発でよく使用するユーティリティの提供。
エンコーディング指定のテキストファイル読み書きや、パターンマッチは std::filesystem には無いユーティリティである。

単純なファイルのコピーや削除であれば std::filesystem と機能的な差は無い。
ただし、短いパスの場合は Path 用の SSO が効くので、ln::String, ln::Path と併用する場合、全体的には std::filesystem より高速に動作する。

動機

Lumino の多くの API は発生しうるエラーをロジックエラー(ユーザーではなくプログラマが悪い)と考え、assertion または例外(選択可能) を投げて通知する。
基本方針は「プログラムを止める」である。

しかしファイル操作(に限らずIO関係はそうだが)はどうしてもプログラムの外部の要素が絡むので、ユーザーエラーは避けられない。
他のラフに使える API と同じような感覚で使うべきではないだろう。

現状、システムに近いネイティブアプリでの導入がある。
ここではスクリプト言語のような感覚で使えてしまう API はかえって危険だろう。

構成ごとの Shader 自動生成

  • #164 書き方の整理
  • 自動生成

Motivation

Instancing 有効、NormalMap 有効、など、構成の組み合わせがかなり増えてきているので、自動的に作りたい。

開発中のタイトル HC4 では背景モデルに対してシェーダを適用したいが、対象は NormalMap 有無など複数の構成を書かなければならず、シェーダ開発が非効率になっている。

Proposal

現状の構成は次の通り。

PS の出力方法 (Phase)

  • Default
  • ShadowCaster -> PS 固定
  • LightDisc -> VS/PS 固定
  • GBufferPrepass -> PS 固定

VS の種類

  • StaticMesh Default
  • StaticMesh Instancing
  • SkinnedMesh Default
  • SkinnedMesh Instancing

Phase が Default のときの PS 種類 (ShadingModel)

  • Default
  • Unlit

PS のオプション

  • NormalMap
  • RoughnessMap

ユーザーカスタマイズを考慮した基本的なシェーダサンプル

方針:VSMain, PSMain は直接書くことにして、オプションの変更はマクロなどで対応する。
Unity のサーフェスシェーダみたいなのを作ることになるとシェーダコンパイラに手を入れる必要があり、かなり重い作業になるため。

struct MyVSOutput
{
	LN_VS_OUTPUT_DATA;	// float3 Pos : SV_POSITION; など
	float4 MyData : TEXCOORD0;
};

struct MyPSInput
{
	LN_PS_INPUT_DATA;
	float4 MyData : TEXCOORD0;
};

MyVSOutput VSMain(LN_VSInput input)
{
	MyVSOutput output;
	LN_ProcessVertex(input, output);	// マクロ。output.Pos などへ代入。スキニングやモーフィングもここで計算する。
	
	output.MyData = /* ユーザー定義データの構築 */;
	
	return output;
}

float4 PSMain(MyPSInput input) : SV_TARGET0
{
	//LN_Surface surface = LN_InitSurface();	// 初期化するだけ
	LN_Surface surface = LN_ProcessSurface(input);	// input.TexUV 等を使って、Albedo など各要素をデフォルト構築する。
	
	surface.Albedo = /* ユーザー定義で模様を付けたりする。デフォルトシェーダでは何もしない。 */;
	
	// surface は Unity の SurfaceShader の Output 相当。
	// LN_ProcessPixel は UE4 の Output ノード相当。
	// LN_ProcessPixel の中で ShadingModel に応じて PBR の計算をしたり、環境光, Fog の適用などが行われる。
	return LN_ProcessPixel(surface);
}

technique Default
{
	pass Pass1
	{
		VertexShader = VSMain;  // VS はほどんと定型になるので、LN_VSDefault とかを用意してもいいかも。
		PixelShader = PSMain;
	}
}

自動生成の方針

  • technique 名が "Default" であるものについてのみ、自動生成を行う。
    • Default 以外はほとんど内部用途なので、がんばって直書きする。
  • 上記構成だけでも、128 個組み合わせができる。実行速度との相談になるが、コンパイルオプション以外の方法で動的に切り替えができそうならそっちを検討する方がよさそう。初期化時間がすごく長くなるので。
    • Instancing 有無は入力頂点の定義を変える必要があるので、コンパイルオプションで。
    • StaticMesh/SkinningMesh は uniform で切り替えられそう。VTF で動かしてるので、そのテクスチャサイズが 0 なら Static、とか if で判断する、でいいかも。
    • ShadingModel はその有無で実行コードがかなり変わる。Unlit なのに PBR 用のコードや uniform が含まれてしまうと、主に C++ 側に初期化時・実行時共に余計なオーバーヘッドが入ってくるので、コンパイルオプションにしたい。
    • NormalMap や RoughnessMap は、青テクスチャや白テクスチャを使うことでデフォルトを表せる。テクスチャレジスタやFetchが増えるが実際に動かしてみたあと、実行速度と相談で。
  • ↑のようにしておくことで、現状の ShadingModel だと組み合わせは 4 パターンで済みそう。

Note

4パターンくらいならまだ手打ちでもなんとかなるので、まずは上記サンプルの形に、ビルトインシェーダを全部直す。

Transform 設定方法の変更

Proposal

// x 座標を直接設定する
sprite->setPositionX(1.0f);

// x 座標を平行移動する
sprite->translateX(1.0f);

// x 軸回転を直接設定する
sprite->setRotation(1.0f);

// x 軸回転を加算する
sprite->rotateX(1.0f);

// x 軸拡大率を直接設定する
sprite->setScale(1.0f);

// x 軸拡大率を加算する
sprite->scaleX(1.0f);

Motivation

現状、例えばX方向に移動するのに↓のように書く必要があるのがめんどい。

sprite->setPosition(Vector3(sprite->position().x + 1.0f, sprite->position().y, sprite->position().z));

公開変数を用いる方法について

WorldTransform の遅延構築について・・・
現在 WorldTransform は、setPosition時に dirty フラグを立てておき、WorldTransform が欲しいときに計算している。

Binding の Wrapper 側で変数を使う場合、この値をどこかで Native 側に渡さなければならない。

しかし上記の例の場合は WorldTransform が position や rotation に依存しているため、WorldTransform get 時に Wrapper 側にコールバックして値をもらう必要がある。
(onPreUpdate などで定期的に Wrapper から Native へ送り付けるのもアリだが、WorldTransform の場合はタイミング的に onPreUpdate などではカバーできない。setPosition() の直後に getWroldTransform() したりもできる)

そのため、公開変数を用意することはできない。

translateX

代替として、setPositionX() による直接設定や、translateX() による相対移動、rotateX() による相対回転などを実装する。

Research

Three.js

Object3D.position などは公開変数である。(getter, setter などは無い)

そのため、obj.position.x += 1.0 のように、要素単位で演算ができる。

Unity

ドキュメント上は「変数」となっているが、ReferenceCode を見るとプロパティとなっている。

右手座標系をサポートする

動機

なぜこれまで左手座標系だったのか?

キャラクターあるいはカメラを起点とした処理を考えやすいため。
特にピクセルシェーダで ViewSpace 上の計算を行うときに Z+ が "進行方向" と一致するためイメージしやすかった。
ClipSpace に至っては OpenGL でも DirectX でも左手座標系であり、低レイヤーに近づくほど左手座標系の方が扱いやすい。

また Lumino の開発初期にリサーチできたゲームエンジンでは、左手座標系が採用されているものが多かった。(Unity, UE4 も左手)

これまではレンダリングエンジンの実装に力を入れていたため左手座標系の方が都合がよかったのだが、より高レベルの機能をサポートし始めたことで次のような問題が出てきた。

左手座標系の問題点

様々な DCC ツールで作成したアセットとの相性が悪いこと。
特に3Dモデルは頂点データの他、ボーンやアニメーション等非常に多くのデータ構造を扱う必要がある。
DCC ツールのほとんどは右手座標系であるが、こういったアセットを正しくインポートするには常に座標系の変換に気を配る必要があり負担が大きい。

またこの変換のためのオーバーヘッドがどうしても乗ってくる。
Lumino が 3D をサポートし始めたころに対応した PMD,PMX は左手座標系であったため問題は少なかったが、その後 glTF を標準的に使用していくことに決めた。Lumino は独自形式を持たないためランタイムでは常にこの変換のオーバーヘッドが発生してしまうことになる。

左手座標系をやめることの問題点

シェーダを除いてほとんど無い。そのシェーダについても、この Issue を上げた時点の rh-proto ブランチでは既にすべてのユニットテストをパスしている。

X軸の意味が変わるが、実際にゲームでキャラクターの動きを作る際には Z(進行方向) と Y(上方向) をイメージしながら作ることが多く、X軸についてはそれらよりも明確な意図をもって使う機会は少ない。ZY の意味が変わらなければ、既に作られている Lumino のユーティリティへの被害も少ない。

面のカリング方向について

CCW を正面とする。("右手" に統一する)

これは glTF・Vulkan の標準的な仕様なので、これに合わせてみる。
他のライブラリだと imgui や nanovg がこれにあたる。

参考

https://note.com/it_ks/n/nb13311509f0a

imgui
VK_FRONT_FACE_COUNTER_CLOCKWISE

vulkan
VK_FRONT_FACE_COUNTER_CLOCKWISE

Three.js
CW?
KhronosGroup/glTF-Blender-IO#551

glTF
CCW

godot
PipelineRasterizationState の初期値を見ると CW

Nuget パッケージで Shlwapi.lib をリンクしてほしい

一部の機能を呼び出すと、リンクエラーとなる。

LNK2019	未解決の外部シンボル __imp__PathMatchSpecExW@12 が関数 "public: static bool __cdecl ln::PlatformFileSystem::matchPath(wchar_t const *,wchar_t const *)" (?matchPath@PlatformFileSystem@ln@@SA_NPB_W0@Z) で参照されました。

プロジェクトに直接 "Shlwapi.lib" をリンクするように設定を加えれば回避可能。

Lumino.Core.targets に

        <Link>
            <AdditionalLibraryDirectories>$(MSBuildThisFileDirectory)../../build/native/lib/;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+            <AdditionalDependencies>Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
        </Link>

とかするとよさそう。

ヘッダで #pragma comment(lib, "Shlwapi.lib") でもいいかも。

エントリーポイント見直し

Lumino はメインループを内部に持つスタンドアロンのアプリケーションフレームワークのほか、別の GUI フレームワークにレンダリングライブラリとして組み込む機能も持っている。

しかし最近は対応する言語や環境が増えてきたことで、暫定的に用意したグルーコードがかなり複雑になってきている。

エントリーポイントのシンプルさはそのままサンプルコードのとっつきやすさにつながってくるため、仕様を整理する。

基本方針

  • ユーザーが使うクラスを増やしすぎない。
  • スタンドアロンのアプリケーションフレームワークでサンプルを示すとき、コードが最小限になること。(驚き最小)
  • 別のフレームワークに組み込む時は、そちらの文化にできるだけ似た書き方ができるようにする。
  • ゲームアプリケーションを実装する用に、 Application クラスをオーバーライドできるようにする。
  • GUI アプリケーションを実装する用に、 Application クラス及び MainWindow クラスをオーバーライドできるようにする。

破壊的変更

これまで ln::Engine::initalize() で MainWindow を自動生成していたが、それをやめる。

v0.10.0 以降、 Lumino 単体で使う際は Application クラスを使う方法が基本となったため、自分で初期化 & メインループを各機会は外部アプリに組み込む時だけになっている。
サッと Lumino を使いたい人にとって ln::Engine::initalize() を使った初期化周りは隠蔽されることになるので、このあたりの仕様は外部アプリに組み込みたいケース向けに調整していく。

Application クラスと MainWindow クラスの関係

いくつかの GUI フレームワークでは Application が先にあって、MainWindow は後から作られる。
しかし Lumino は MainWindow が先となるので注意。

他の GUI アプリに組み込む場合、 Application はオプションとなる。
対して MainWindow は描画先サーフェスを表すものであり、単なるレンダリングライブラリとして使う場合でも必須である。
(Window と Surface を分離するのもありだが、いずれにしても Application よりも優先されるインスタンスとなる)

Standalone

C++ (with launcher)

class MyApp : public ln::Application {
    MyApp() {
    }

    void onInit() {
        // この時点で内部・外部問わず作成された MainWindow を ln::Engine::mainWindow() で取得できる。
    }
}

LN_APP(MyApp, MyMainWindow)
// Launcher例 (Libになっているので、基本はユーザーが書くことは無い)
LuminoConfigureApp();
ln::Engine::initialize();
ln::Engine::setupMainWindow(LuminoCreateMainWindow());
ln::Engine::setupApplication(LuminoCreateApp());
ln::Engine::run();

C++ (without launcher)

Launcher 未対応の環境で使う。あまり機会は無いかも。

class MyApp : public ln::Application {
    MyApp() {
    }

    void onInit() {
    }
}
ln::Engine::initialize();
auto app = makeObject<MyApp>();     // Application のコンストラクタで、MainWindow 未設定なら ln::Engine::setupMainWindow() 実行、でいいかも。
app->run();
ln::Engine::terminate();

C

// init
LNHandle mainWindow, app;
LNEngine_Initialize();
LNMainWindow_Create(&mainWindow);
LNMainWindow_SetPrototype_...(mainWindow, ...);
LNEngine_SetupMainWindow(mainWindow);
LNApplication_Create(&app);
LNApplication_SetPrototype_...(app, ...);
LNEngine_SetupApplication(app);

// run
LNEngine_Run();

Ruby

require "lumino"
Engine.initialize   # require の中で initialize してしまうと EngineSettings による設定ができないので、明示的な初期化が要る。(あるいは App のコンストラクタで呼んでもいいが)
app = new App()
app.run()
require "lumino"
Engine.initialize
Engine.setupMainWindow
app = new App();
app.run()
Engine.terminate

TypeScript

ExternalMainLoop 必須。

External main loop

C++

// init
ln::Engine::initialize();
ln::Engine::setupMainWindow(makeObject<MainWindow>(hWnd));  // ユーザー定義の MainWindow を登録。この仕組みは App とは直接関係は無い。
ln::Engine::setupApplication(makeObject<App>());

// update
ln::Engine::update();

// render
ln::Engine::render(ln::Engine::mainWindow());
swap(hWnd);

// terminate
ln::Engine::terminate();

C++ (External GraphicsAPI)

// init
ln::EngineSettings::setGraphicsBackend(GraphicsBackend::None);
ln::Engine::initialize();
auto renderer = ln::VulkanIntegration::createContext(device);
ln::Engine::setupGraphics(renderer);
ln::Engine::setupMainWindow(makeObject<MainWindow>(hWnd));
ln::Engine::setupApplication(makeObject<App>());

// update
ln::Engine::update();

// render
renderer->resetCommandList(...);
ln::Engine::render(renderer, ln::Engine::mainWindow());
swap(hWnd);

// terminate
ln::Engine::terminate();

Ruby

# init
require "lumino"  # この中で initialize() したいところだが、 require の前で EngineSettings を呼ぶことができないため断念。
Engine.setupMainWindow(new MainWindow(window_id))
Engine.setupApplication(new App)

# update
Engine.update

# render
Engine.render(ln::Engine::mainWindow());
swap(window_id)

TypeScript

// init
const renderer = WebGpuIntegration.getContext(device);
const mainWindow = new MainWindow(canvas);
Engine.setupMainWindow(mainWindow);
Engine.setupApplication(new App());

// update
Engine.update();

// render
renderer.resetCommandList(webgpuCommandList);
Engine.render(renderer, mainWindow, w, h);

RenderingContext の draw 系関数から Style に関係するステートを削除する

Proposal

例えば、drawText のシグネチャは次のようになっている。

void drawText(const StringRef& text, const Color& color, Font* font);

これを、次のようにしたい。

void setTextColor(const Color& color);
void setFont(Font* font);
void drawText(const StringRef& text);

Motivation

drawText を UIElement のカスタム実装で使おうとしたときに、既定の設定を使うためには color と font は finalStyle() から取り出す必要がある。

この処理のため定型的なコードが増えてしまい煩わしい。

Implementation

ステートレス VS ステートフル

ステートレス方式はいわゆる数学的に単純であるが、パラメータが多くなるためほとんどのケースでは冗長なコードが増え、メンテナンスが難しくなる。

ステートフル方式はパラメータが少なく簡単に使えるが、プログラマが現在の状態を正しく認識することが難しく、不具合が発生しやすくなる。

ステートリセットと Push/Pop

ステートフル方式の問題の軽減策として、ステートの Reset と Push/Pop による復元をサポートする。

Note

実際のところステートは Font や Color だけではなく、RenderTarget や BlendState など多岐にわたる。

もし完全ステートレスを目指すなら DX12 や Vulkan の Pipeline 相当のデータ構造が必要になるが、パフォーマンスを徹底的に追求するならまだしも、
フレームワークというものが目指すものである「制約を設けて開発速度を上げる」からは逆の考えになってきそうなので思い直した方がよさそう。

コメントアウト

たくさんの場所でコードのコメントアウトがありますが、gitを使っているんですからそっちに任せませんか?

ビルドができない

cmakeをインストールしてbuild_msvcを実行したのですが

C:_prog\Lumino\build_vs2015_u>cmake ".." -G "Visual Studio 14 2015" -DLN_USE_UNICODE_CHAR_SET=ON
CMake Error at CMakeLists.txt:749 (add_subdirectory):
The source directory

C:/_prog/Lumino/external/Lumino.Core

does not contain a CMakeLists.txt file.

CMake Error at CMakeLists.txt:750 (add_subdirectory):
The source directory

C:/_prog/Lumino/external/Lumino.Math

does not contain a CMakeLists.txt file.

CMake Error at CMakeLists.txt:753 (ln_make_postfix):
Unknown CMake command "ln_make_postfix".

-- Configuring incomplete, errors occurred!
See also "C:/_prog/Lumino/build_vs2015_u/CMakeFiles/CMakeOutput.log".

C:_prog\Lumino\build_vs2015_u>MSBuild ".\Lumino.sln" /t:Rebuild /p:Configuration="Release" /p:Platf
orm="Win32" /m
Microsoft (R) Build Engine バージョン 4.6.1055.0
[Microsoft .NET Framework、バージョン 4.0.30319.42000]
Copyright (C) Microsoft Corporation. All rights reserved.

MSBUILD : error MSB1009: プロジェクト ファイルが存在しません。
スイッチ:.\Lumino.sln

と出てしまいビルドができません。

UIEventHandler の登録 API を整理する

[2021/1/6] やっぱり現状維持にしてみる

Builder パターンの導入を始めたので、そっちで対応してみる。C++ から使うときはほとんどこのケースで足りる。

auto button = UIButton::Builder()
    .onClicked(handler)
    .build();

新しめの UI フレームワークは Builder パターンないしそれに近い、副作用の少ない方法で UI要素を構築するケースが多い。これで十分な感じ。

connectXX は主に Binding を作るとき、「これはイベントハンドラの登録関数です」を強調するのに使うことにしてみる。

イベント名は過去形?

ネイティブに近いフレームワークは過去形が多い傾向。
ただ過去形に統一しすぎると MouseDowned とかなじみ無い名前になる。

タイミングを区別するなら WPF の "Preview" Prefix のようなものを付ける、でいいかも。

Proposal

connectOnXXXX 及び onXXXX のインターフェイスを変更する。
例えば UIButton の Clicked イベントであれば、

Ref<EventConnection> connectOnClicked(Ref<UIEventHandler> handler);
virtual void onClicked(UIEventArgs* e) override;

UIButton* onClicked(Ref<UIEventHandler> handler, Ref<EventConnection>* outConnection = nullptr);
virtual void onClickedEvent(UIEventArgs* e) override;

ただし、on~ は他のモジュールでも使用している仮想関数実装の命名規則でもあるため、上記そのまま適用は難しいかもしれない。

候補としては、

  • setOnClicked
    • Ruby や C# binding で使うときに on_clicked や OnClicked となるため、他モジュールの命名規則と衝突する。
  • setClickedHandler
    • まだちょっと長いかも…。特にスクリプト系 Binding から見た時。
  • setClicked
    • 落としどころかも?通常プロパティと区別するため、イベント名には名詞を使わないように注意する必要がある。

Motivation

現在、ユーザープログラムから、イベントをハンドリングする方法は次の2通り。

  • A. connectOnXXXX にコールバックを登録する
  • B. コントロールを継承して仮想関数 onXXXX を実装する

ユーザープログラムから見ると、このうち A は非常によく使い、B はあまり使わない。
(A はコントロールを利用するための API であるが、B はコントロールをカスタマイズするための API)

そのため以下のような不満が出てきた。

  • A のほうが頻繁に使うのにタイプ量が多い
  • 戻り値が決まっているので Builder パターンのようなメソッドチェインが組めない
  • EventConnection はほとんどのケースで使用しないが、そうすると VisualStudio でインテリセンスの警告が出る → C26444

Note

on~ を採用しているものはそれなりにある。ただ、signal ではなく普通のプロパティとして単一のハンドラをセットするケースが多い印象。

C# の Event や、signal-slot の仕組みで作られているものは on~ ではないことが多いみたい。

WPF

  • MouseEnter += _handler

Material-UI

  • onClick = _handler

onClick の呼び出し元は handleClick.
ただ仮想関数という考え方ではない。

一番根っこは div の onClick にローカル定義した関数をセットしているだけ。
<div ... onClick={handleClick} ...>

JavaScript

Flutter

  • onPressed: _handler

Unity(UIElements)

  • clicked += _handler
    ※ onClick もあるが、Obsolete としてマークされている。また、コンストラクタでもハンドラを受け取れる。

これらはすべて C# の event として実装されている。

  • Button.clicked
  • ListView.onSelectionChanged

仮想関数は次のような命名になっている。

  • Clickable.OnMouseDown()
  • Clickable.OnMouseMove()

UE4

  • (BP) On Clicked
  • (Slate) OnClicked(_handler)

イベントと仮想関数の命名規則に区別はない。

  • OnMouseButtonDown()
  • OnPaint()
  • OnClicked
  • OnAcceptDrop
  • OnPaintHandler

Kivy

  • (.kv) on_press: _handler
  • (.py) on_press=_handler (コンストラクタ名前付き引数)
  • (.py) b.bind(on_press=anim_btn)

イベントと仮想関数の命名規則に区別はない。

  • on_touch_down()
  • on_touch_move()
  • on_touch_up()
  • on_press:
  • on_release:

Qt

  • (QWidgets) connect(a, SIGNAL(valueChanged()), b, SLOT(_handler));
  • (QML) onClicked: _handler

Android

  • setOnClickListener(_handler)

UIKit

  • button.addTarget(self, action: #selector(_handler),

GTK

  • g_signal_connect (button, "clicked", G_CALLBACK (_handler), (gpointer) "button");

Rust azul

let button = Button::with_label("+1").dom().with_id("button")
      .with_callback(On::MouseUp, Callback(update_counter));

Rust gtk

button.connect_clicked(move |_| { ... });

Rust iced

Button::new(&mut self.reset_button, Text::new("reset"))
    .on_press(Message::Reset),

OrbTk iced

self.on_mouse_up(move |p| { ... });

Note

現時点で Event<> な変数は次の通り。

canUndoChanged;
canRedoChanged;

m_onCollisionEnter;
m_onCollisionLeave;
m_onCollisionStay;
m_onUIEvent;
m_onChecked;
m_onUnchecked;

m_onActivated;
m_onDeactivated;

m_onSubmit;
DragStarted;
DragDelta;
DragCompleted;
DragCanceled;

m_onItemSubmitted;
m_onGenerateTreeItem;

m_onCanExecuteChanged;
m_onCanExecuteEvent;
m_onExecuteEvent;

m_onClosed;
m_onImGuiLayer;

m_onClick;
m_onItemClick;
m_onSelectionChanged;
m_onSelectedTabChanged;

m_onRender

onGenerateTreeItem, onRender あたりは他の名前にし辛いかも。

  • (C++) setRender(...)
  • (C#) Render += ...
  • (Ruby) render = ...

onUIEvent は動詞でもないケースなので、Ruby とかだと普通の変数と区別ができなくなる。
スクリプト系だとこのあたりの視認性が悪くなるのはマイナス。

Assets モジュールの強化

  • 任意のフォルダを Assets フォルダとして登録
  • 文字列の読み込み
  • ファイルを開いて Stream として返す

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.