ハスケル-美学

宇宙シミュレーターのジャンルで特別なゲームを思いつきます。 重要な概念の1つによれば、ゲームには組み込みのプログラミング言語があり、これを使用して、ゲーム要素の相互作用のアルゴリズムを開発および改善できます。 そのような言語の設計は、「テキストの性質」ではなく「自然さ」を考えれば簡単な作業ではありません。 つまり、言語構造は異なるグラフィックオブジェクトとして表現されます。 彼のデザインのスケッチを描くと、私は突然自分に気を取られ、ゲームの言語ではなく、Haskellコードを視覚化するための言語を思いつき始めました。 とても興味深いことがわかったので、スケッチを紙の絵だけで残すことはできませんでした。 2012年1月、視覚化サーバーの作成を開始しました。





何かを視覚化するには、まずそれを意味のある単位に解析してから、グラフィック要素をマッピングする必要があります。 コードの場合、そのようなユニットは言語の構文要素になるため、最初のステップでコードを抽象構文ツリー(AST)に解析する必要があります。 GHCコンパイラーは、これを何よりもうまく行う方法を知っており、これを行うバインダーさえあります。 Scionは、GHC APIを使用してコードを分析できるライブラリです。 Scionは、たとえば、EclipseFPで構文の強調表示とエラー分析をその場で使用します。 そして、開発の初期段階では役に立たなかった彼の複雑さがなければ、彼は良いでしょう。 パーサーを手動で記述したくありませんでした。 不思議なことに、シンプルでありながら十分な別の方法がありました:Language.Haskellライブラリ。

Language.Haskell.Parserモジュールは、Haskell'98コードの拡張機能なしのクリーンなパーサーです(まあ、拡張機能はほとんどありません)。 「高度な」プログラムは解析できませんが、最初はHaskell'98自体で十分です。 実験ウサギとして、階乗計算コードを取りました:

ファクト 'n | n == 0 = 1
| それ以外の場合 = fact ' n - 1 * n


ライブラリを使用した解析と分析は基本的に行われます。簡単なプログラムの例を次に示します。

言語をインポートし ます。 ハスケル パーサー

メイン= 行う
s < -readFile "Fact.hs"
let parsed = parseModule s
putStrLn $解析済みを表示


parseModule関数のタイプは次のとおりです。

parseModule :: String- > ParseResult HsModule


ここで、最初の引数はHaskellコードであり、タイプHsModuleの値が返されます。 HsModuleタイプを使用するには、Language.Haskell.Syntaxモジュールを接続する必要があります。 その型構造は、Haskell'98サブセットをASTとして完全に記述しています。

パーソク
HsModule
SrcLoc { srcFilename = "<unknown>" srcLine = 3 srcColumn = 1 }
モジュール"メイン"
Just [ HsEVar UnQual HsIdent "main" ]
[ ]
[ HsFunBind
[ HsMatch
SrcLoc { srcFilename = "<unknown>" srcLine = 3 srcColumn = 1 }
HsIdent "fact '"
[ HsPVar HsIdent "n" ]
HsGuardedRhss
[ HsGuardedRhs
SrcLoc { srcFilename = "<unknown>" srcLine = 3 srcColumn = 9 }
HsInfixApp
HsVar UnQual HsIdent "n" ))
HsQVarOp UnQual HsSymbol "==" ))
HsLit HsInt 0
HsLit HsInt 1
HsGuardedRhs
SrcLoc { srcFilename = "<unknown>" srcLine = 4 srcColumn = 9 }
HsVar UnQual HsIdent "otherwise" ))
HsInfixApp
HsApp
HsVar UnQual HsIdent "fact '" ))
HsParen
HsInfixApp
HsVar UnQual HsIdent "n" ))
HsQVarOp UnQual HsSymbol "-"
HsLit HsInt 1 ))
HsQVarOp UnQual HsSymbol "*" ))
HsVar UnQual HsIdent "n" )) ] [ ] ] ]


GraphServerプロジェクトでは、ASTツリーをコンポーネントに分割して、作業しやすくしました。

t1 = HsInfixApp HsVar UnQual HsIdent "n" ))
HsQVarOp UnQual HsSymbol "-"
HsLit HsInt 1

t2 = HsApp HsVar UnQual HsIdent "fact '" ))
HsParen t1

t3 = HsInfixApp t2
HsQVarOp UnQual HsSymbol "*" ))
HsVar UnQual HsIdent "n" ))

t4 = HsGuardedRhs SrcLoc { srcFilename = "<unknown>" srcLine = 4 srcColumn = 9 }
HsVar UnQual HsIdent "otherwise" ))
t3
...


サーバーが通常モードで動作する場合、Haskellコードの行をサーバーに送信できます。このコードは、サーバーによってASTに解析され、視覚化されます。 現在、開発のアクティブフェーズでは、サーバーはアイドル状態です。 単に1つまたは別の「t関数」を選択し、視覚化プロセス全体を開始します。 しかし、見かけの単純さの下では、さまざまなメカニズムとアルゴリズムが隠されており、その結果を図で見ることができます。



セキュリティ式と関数の適切な部分を次に示します。

| n == 0 = 1
| それ以外の場合 = fact ' n - 1 * n



グラフィック言語に構文構成の意味を反映させたいと思いました。 ガード式は「実行フィルター」のように機能します。 思考実験でフレームの直観性を示しましょう。 進行状況は、フレームを通過して関数の本体に到達するキューブ、またはサイズに応じて通過しないキューブ(「論理条件」)を想像してください。 上の画像の矢印は進行状況を示しています。 おもしろいことに、フレームは、正面からシーンを見ると、コードに見られる非常に垂直な線に変わります。


オペレーターによって作成されたリストがどのように見えるかを理解しようとしました:パターンマッチングの内部で、これまでのところ、写真のオプションを思いつきました。 最初は式(x1:x2:xs)に対応し、2番目は式(x1:_:[])に対応します。 「重要ではない」要素の代わりに、平らなプラットフォームと空のリストが描かれています-それは空です。 条件付きifステートメントとcase-constructionも多少直感的です。 私は紙の上に他のラフなスケッチを持っていますが、Haskell構文の多くはまだ開発する必要があります。 これは、型宣言、およびその機能を備えたdo-construction、パターンマッチング、およびその他の重要な事項にも適用されます。 そして、作成されたスケッチをコードに組み込む必要があります...



そのため、タスクは構文要素のグラフィック要素を生成し、何らかの方法でそれらを配置して1つのシーンに結合し、それを描画することです。 知っているように、ある言語が別の言語に変換されるプロセスはコンパイルと呼ばれます。 いくつかのテストが失敗した後、段階的なコンパイルに至りました。このコンパイルでは、コードはまだ混乱せず、補足することができます。 手順は次のとおりです。

I. AST要素からStructureObject要素への変換
II。 構造プリミティブをStructureObject要素にマップする
III。 StructureObjectを相互に組み合わせて配置する
IV。 グラフィックプリミティブのシーン要素へのコンパイル
V.シーンのレンダリング

実装に移る前に、サーバーコードが分割されるモジュールを検討します。

GraphServer(メイン)-プログラムのメインモジュール。 主な機能であるサーバーロジックが含まれています。 その中で、OpenGLの初期化、ウィンドウの作成、初期設定が行われます。 メインプログラムループもそこにあり、Draw.Drawモジュールの描画関数がその中で回転しています。

共通-一般的なデータとアルゴリズム。
Common.Constants-一般的な定数、設定、固定データ。
Common.GLTypes-OpenGLタイプ(ベクトル、頂点など)、およびその他の定義。
Common.TestData-「t関数」、テストメカニズムの追加データ。
Common.Units-OpenGLベクトルと頂点を使用して、空間単位を操作するための関数。

構造-ASTツリーをStructureObjectツリーにコンパイルするためのデータ型とアルゴリズム。
Structure.Constants-ステージI、II、IIIに関連する定数と設定。
Structure.StructureObject-StructureObjectの中心的なデータ型の説明。
Structure.GraphObject-GraphObjectタイプの説明と、このタイプのオブジェクトを作成する関数。
Structure.Dimensions-グラフィックオブジェクトの寸法を操作します。
Structure.SOConstruct-StructureObjectを作成します。 ステージI、IIに対応します。
Structure.SOConnect-複数のStructureObjectの接続。 ステージIIIに対応します。
Structure.GOCompile-グラフィックプリミティブ(GraphObject)を実際のシーンオブジェクトにコンパイルします。 ステージIVに対応します。
Structure.HsSyntaxTools-ASTを操作するためのヘルパー関数。
Structure.Texture-テクスチャを操作するための補助データ型と関数。

Draw-シーンのレンダリングを担当する関数。
Draw.Draw-シーンがコンパイルおよびレンダリングされる描画機能が含まれています。
Draw.GLInit-名前が示すように、OpenGLを初期化するために設計されたヘルパー関数。
Draw.Render-レンダリング機能が含まれています。 ステップVに対応します。
Draw.TextureInit-テクスチャを作成するためのヘルパー関数。

その他-その他の補助機能。
Misc.BoxSide-ボックスの端が作成されるHOpenGLライブラリの関数。

ThirdParty-サードパーティのユーティリティとプログラム。
ThirdParty.Frag-Fragプログラムのコード。 TGAファイルをダウンロードし、テクスチャを作成します。
ThirdParty.GLUtil-OpenGLを操作するための追加ユーティリティ。
ThirdParty.ImageFormats-TGAファイルをダウンロードします。



上記のdraw関数のアルゴリズムをテストして実行します。

draw :: DrawFunction
GLResourcesを描画texRes n = do
putStr $ "Current n =" ++ ショー n
GL クリア[ GL ColorBuffer GL DepthBuffer ]
GL loadIdentity
GL 回転10 vector3 0 1 0
GL 20回転 vector3 1 0 0
GL 翻訳 vector3 -5 - 10 - 30

-StructureObjectから階層を構築します
let c = constructFramedGRhss OcsGuardedRhss t6

-グラフィックプリミティブからシーン要素をコンパイルする
-そしてシーンを描く
texRes cをレンダリングします
putStrLn 「OK」。




constructFramedGRhssおよびrender関数はすべての作業を行い、出力では記事の冒頭に示されている画像を取得します。 Structure.SOConstructモジュールのconstructFramedGRhss関数(およびその類似物)は、ステップI〜IIIを実装します。 彼女にはこのタイプがあります:

constructFramedGRhss :: ObjectConstructSpec- > StructureObject


特定のObjectConstruct仕様を受け入れ、StructureObjectから既成の階層を返します。 仕様は単なるADTであり、どの構文単位を扱っているかを詳しく説明しています。

-モジュールStructure.StructureObject
データ ObjectConstructSpec
= OcsApp HsExp
| OcsExpArgument HsExp
| OcsExpFuncName HsExp StructureObject
| OcsInfixOperator HsQOp
| OcsGuardedRhs HsGuardedRhs
| OcsFoundationExp StructureObject
| OcsGuardedRhss HsRhs
| Ocsarrowbridge
| OcsEqualSignBridge
| OcsMatch HsMatch


StructureObjectデータ型は、可能な階層構造を記述するのに十分な汎用性が必要です。

-モジュールStructure.StructureObject
データ StructureObject = StructureObject
{ soObjectSpec :: ObjectSpec
soGeometry ::ジオメトリ
soGraphObjectSpec :: GraphObjectSpec
soStructureObjects :: StructureObjects
} 導出 表示

タイプ StructureObjects = [ StructureObject ]


ご覧のとおり、soStructureObjectsフィールドには子オブジェクトのリストが含まれています。 実際、AST階層は、位置、サイズ、グラフィックプリミティブ、テクスチャなど、レンダリングに必要な情報が蓄積されたStructureObject階層に変換されます。 ツリーは、最下位レベルから構築されます。これは、上にあるレベルの要素を空間のどこに配置するかを決定する唯一の方法だからです。 このスキームは、シーン空間内のStructureObjectオブジェクトに絶対座標をすぐに割り当てることができないという制限を課します。子オブジェクトから上に移動すると、親要素がどこにあるかさえ想像できません。 したがって、すべてのStructureObjectは、その親に対してのみ配置できます。 つまり、各StructureObjectには、親のゼロ点を基準にしたOX、OY、およびOZ軸に沿った独自のオフセットがあります。 次のようになります。



この図は、通常2つのStructureObjectを示しています。白いパネルは親オブジェクトのスペースに対応し、ブリックパネルは子のスペースに対応しています。 StructureObject自体はシーンに表示されませんが、グラフィックオブジェクトのコンテナーと見なされます(図の青色)。 グラフィカルオブジェクトは、含まれるStructureObjectのゼロポイントを基準にして配置されます。 次元(次元)StructureObjectは、すべての下位構造の一般的な次元であり、親要素での計算に必要です。 soGeometryフィールドのタイプはGeometryです。 これには、3次元ベクトルによって定義された一般的な変位と寸法が含まれます。

-Common.GLTypesモジュール
タイプ Geometry = Translation Dimension
GLfVector3 = GLと入力します。 Vector3 GL GLfloat
タイプ Translation = GLfVector3
タイプ Dimension = GLfVector3


同様のデータとグラフィックオブジェクトには、次のタイプのsoGraphObjectSpecフィールドが含まれています。

タイプ GraphObjectSpec = Translation Dimension GraphObject


StructureObjectを作成するためのコードは広範囲にわたり、2つのモジュールに分かれています。 構文要素(単純および複雑)は、Structure.SOConstructモジュールでStructureObjectsに変換されます。 最初は、soGeometryフィールドのTranslationオフセットはゼロベクトルに等しくなります。 新しく作成されたStructureObjectが親に対してどのように配置されているかを単に知ることができず、親はまだ存在すらしておらず、必ずしも将来表示されるとは限りません。 オフセットは、子が作成するコードで後で修正されます。 または、単にゼロのままです-サブオブジェクトの参照ポイントになるのはこのオブジェクトです。 以下のコードでは、3つの単純なオブジェクト(「変数」、「プラットフォーム」、「ブリッジ」)と1つの複合(引数を持つ中置演算子)の作成:

-変数式のオブジェクトが作成されます:
constructExp OcsExpArgument HsVar var = let
varText = makeName getHsQualName $ var
rawDim = GL Vector3 hsNameLength varText 2 2
dim = derivedDimensions FuncDimensions variableBoxDims rawDim
graphObjSpec = variableBox varText dim
StructureObject OsArgument nullVector3 dim graphObjSpec [ ]

-「プラットフォーム」の構築中:
constructFoundation :: ObjectConstructSpec- > StructureObject
constructFoundation OcsFoundationExp expSo = let
expSoDim = geometryDim soGeometry $ expSo
dim = derivedDimensions FoundationDimensions expSoDim
graphObjSpec = FoundationBox dim
StructureObject OsFoundation nullVector3 dim graphObjSpecで

-「ブリッジ」の構築中:
constructBridge :: ObjectConstructSpec- > StructureObject
constructBridge ocsBridgeType = let
dim = vector3 2 0.25 2
graphObjSpec bType = case ocsBridgeType
OcsArrowBridge -> arrowBridgeBox dim OsArrowBridge
OcsEqualSignBridge -> equalSignBridgeBox dim OsEqualSignBridge
StructureObject bType nullVector3 dim graphObjSpec [ ]

-複雑なオブジェクトが構築されます-引数を持つ中置演算子:
constructExp OcsExpArgument HsInfixApp exp1 qOp exp2 = let
exp1So = constructExp OcsExpArgument exp1
qOpSo = constructQOp OcsInfixOperator qOp
exp2So = constructExp OcsExpArgument exp2
connectStructureObjects OsInfixApp [ exp1So qOpSo exp2So ]


複雑なオブジェクトを作成するときは、何らかの方法で子を配置する必要があります。 異なる構文単位に対して異なる位置があることは明らかです。 オブジェクトを計算して、ゼロを基準としたオフセットを割り当てる必要があります。 新しく作成されたオブジェクトである親は、シフトする量とその対処方法がまだわからないため、ゼロにシフトされます(つまり、まったくシフトされません)。 オフセットは、Structure.SOConnectモジュールのconnectStructureObjects関数によって計算されます。 構文単位のビューと、以前に作成されたサブオブジェクトのリストがそこに転送されます。 各ケースについて、connectStructureObjects関数には独自の計算オプションがあります。

connectStructureObjects :: ObjectSpec- > StructureObjects- > StructureObject

-中置演算子と2つの式が接続されています:
connectStructureObjects OsInfixApp exp1So:opSo:exp2So: [ ] = let
exp1SoDim @ GL。Vector3 e1dl e1dh e1dw = geometryDim soGeometry $ exp1So
exp2SoDim = geometryDim soGeometry $ exp2So
opSoDim @ GL。Vector3 opdl opdh opdw = geometryDim soGeometry $ opSo
exp1Trans = nullVector3- 式1は親ゼロから始まります
opTrans = vector3 e1dl 0 0- 演算子は式1(その隣にある)の長さだけOXだけシフトされます
exp2Trans = vector3 e1dl + opdl 0 0- 式2は式1の長さと演算子の長さだけOYだけシフトされます
generalDim = generalizedDimension [ exp1Trans exp1SoDim -親の一般的な寸法
exp2Trans exp2SoDim
opTrans opSoDim ]
newOpGoSpec = opTrans opSoDim graphObjectFromSpec。soGraphObjectSpec $ opSo
newExp1So = exp1So { soGeometry = exp1Trans exp1SoDim }
newExp2So = exp2So { soGeometry = exp2Trans exp2SoDim }
StructureObject OsInfixApp nullVector3 generalDim )の newOpGoSpec [ newExp1So newExp2So ]


座標軸によって描かれた空の空間を想像してください。 座標の中心-ゼロ。 右ボックスをシフトして、シーンに精神的に追加します。 次に、私たちと左にシフトした、異なるサイズのボックスを追加します。 これらのボックスの両方が親のサブオブジェクトである場合、それらのエッジはスペースを制限します。 上の図では、StructureObjectが明確に表示されています。オブジェクトのスペースは、コンテンツによって決定されます。 しかし、親の総寸法を計算する方法は? 各サブ要素の変位とサイズを考慮し、座標の一般的な最小値、最大値を見つける必要があります。 次に、高値から低値が差し引かれ、全体のサイズが取得されます。 オブジェクトの端に沿って平面を描き、目的の空間の輪郭を描きます。 このアルゴリズムは、オフセットとサイズのリストの畳み込みによって適切に実装され、関数はgeneralizedDimensionと呼ばれます。

-モジュールの構造。寸法
generalizedDimension ::ジオメトリ->ディメンション
generalizedDimension g:gs = toDimension foldr f g gs
どこで
f GL。Vector3 dx1 dy1 dz1 GL。Vector3 ax1 ay1 az1
GL。Vector3 dx2 dy2 dz2 GL。Vector3 ax2 ay2 az2 =
vector3 min dx1 dx2 min dy1 dy2 min dz1 dz2
vector3 max dx1 + ax1 dx2 + ax2
最大 dy1 + ay1 dy2 + ay2
最大 dz1 + az1 dz2 + az2
toDimension GL。Vector3 x1 y1 z1 GL。Vector3 x2 y2 z2 =
vector3 abs x2 - x1 abs y2 - y1 abs z2 - z1



Structure.SOConstructおよびStructure.SOConnectモジュールの面倒な機能にもかかわらず、私はまだ良いものを思いつきませんでした。 おそらくある種の宣言的な解決策がありますが、コードが少なくなる可能性は低いです。 単一の宣言スキームに収めるのが難しい特殊なケースがあります。 そのため、言語の設計によれば、関数は高さが1単位の箱のように見え、その引数はその上にあります。 したがって、ボックスの長さを計算するには、引数の数、サイズ、および引数間の距離を考慮する必要があります。 グラフィック言語の表現はピラミッドのように見えます。つまり、各下層の突起に関連して追加の計算が発生します。 このタスクを要約すると、Structure.DimensionsモジュールのderivedDimensionsが重要な役割を果たす「継承サイズのメカニズム」を作成しました。 要素の初期サイズ、必要な子要素のサイズ、および高階関数として表される継承アルゴリズムを取り、新しい「継承された」サイズを返します。 以下は、pre-pre-previousリストのconstructExp関数からの簡略化されたコードです。

させる
rawDim = GL Vector3 1 2 2
dim = derivedDimensions FuncDimensions variableBoxDims rawDim
...


ここで、rawDimは変数の元のボックスサイズであり、dimは新しい「継承」サイズです。 FuncDimensionsデータコンストラクターは、DerivedDimensions特殊データ型に属します。

-モジュールの構造。寸法
データ DerivedDimensions = FuncDimensions GLfVector3- > GLfVector3
| FoundationDimensions


derivedDimensionsおよび高階関数は、次のように定義されます。

-モジュールの構造。寸法
derivedDimensions :: DerivedDimensions- > GLfVector3- > GLfVector3
derivedDimensions FuncDimensions f dim = f dim
derivedDimensions FoundationDimensions GL。Vector3 l h w = vector3 l + 2 0.25 w + 2

-DerivedDimensionsに配置する関数
-| 引数dimsに従って関数ボックスの寸法を計算します
funcBoxDerivedDims :: GLfVector3- > GLfVector3- > GLfVector3
funcBoxDerivedDims GL。Vector3 opl oph opw GL。Vector3 fBoxl fBoxh fBoxw =
GL。Vector3 f opl fBoxh fBoxh max opw fBoxw
どこで
f opボックス| op > = box = op + 1
| box - op < 1 = op + 1
| box - op > = 1 =ボックス

-| 可変ボックスの調光を計算します
variableBoxDims :: GLfVector3- > GLfVector3
variableBoxDims GL。Vector3 varl varh varw =
GL。Vector3 if varl < 2 then 2 else varl varh varw



ご覧のとおり、derivedDimensions関数のFoundationDimensionsコンストラクターには、元のサイズが特定の量だけ単純に変更される単純なアルゴリズムが指定されています。 長さと幅が2増加し、高さが0.25になります。 より複雑なケースは、funcBoxDerivedDimsおよびvariableBoxDimsを使用して実装されます。 たとえば、その単純化されたコードのdimはGL.Vector3 2 2 2に等しくなります。計算がvariableBoxDims(GL.Vector3 1 2 2)の呼び出しに削減されるためです。 必要に応じて、他の同様の関数を作成できます。 さらに直感的なグラフィック言語のために、将来的に機能のアリティを追加する予定です。 箱の溝のように見えます。 空のスロットはカレーまたはセクションに対応します。 もちろん、継承されたサイズのメカニズムだけでは十分ではありません。アリティを判断するには、単純な構文への分解よりも高度なコード分析が必要です。 しかし、それは別の話です...

興味深いのは、GraphObjectデータタイプ-プリミティブ、テンプレート、空白、将来のシーン要素のプロトタイプです。 コンパイルの初期段階では、プリミティブの頂点の配列全体を正確に知る必要はありません。最初に何らかの種類の空白を設定してから、実際の頂点、線、面に展開する方が簡単です。 したがって、グラフィカル表現から抽象化し、必要に応じてそれを変更したり、別のものに置き換えることもできます。

-モジュールStructure.GraphObjec
データ GraphObject = NoGraphObject
| PrimitiveBox GLfVertex3 TextureName
| TexturedBox GLfVertex3 ObjectTextureSpec
| GraphObjects [ GraphObjectSpec ]
導出 表示


StructureObjectは、グラフィックオブジェクト(GraphObjectsコンストラクター)と同じ数だけ持つことができ、まったく持たない(NoGraphObjectコンストラクター)ことは簡単にわかります。 これは理解できることです。上に表示されているASTツリーでは、HsGuardedRhssの値に対して、グラフィックに一致するものはありません。 むしろ、他のオブジェクト、つまりガード式を含む関数の右側部分(Rhss-「右側」)のコンテナになります。 同時に、いわゆる「実際の」グラフィックオブジェクトは、プリミティブボックスPrimitiveBoxと高度なボックスTexturedBoxの2つの要素のみで表されます。 両方のボックスのタイプの値はGLfVertex3です。これらは、ステージIVで作成されるテクスチャフェース(6個)のサイズにすぎません。 プリミティブボックスには1つのテクスチャがありますが、TexturedBoxの場合は、各面に個別のテクスチャを設定できます。 ObjectTextureSpecタイプは次のように構成されています。

-モジュール構造。テクスチャ
データ ObjectTextureSpec = BoxTextureSpec
{ quadSideTexes :: [ BoxSide QuadColorSpec ]
defQuadSideTex :: QuadColorSpec
} 導出 表示

データ QuadColorSpec = QuadTexture TextureName
| QuadPlainColor GLfColor4
| NoQuadColorSpec
導出 表示

-BoxSideタイプは、Common.GLTypesモジュールで説明されています。
-彼が何であるかを推測するのは簡単です:
データ BoxSide = SideTop
| サイドボトム
| サイドレフト
| サイドライト
| Siderar
| サイドフロント
導出 Show Eq


上部に矢印があり、残りはすべてデフォルトでテクスチャリングされますか? 問題ありません!

let texes = [ SideTop QuadTexture arrowTex ]
defaultTex = QuadTexture yellowBaseTex
boxTexSpec = BoxTextureSpec texes defaultTex


それとも、テクスチャを備えた2つの面と、その他の色を備えた面ですか? そしてそれは可能です。

let texes = [ SideFront QuadTexture arrowTex
SideRear QuadTexture arrowTex ]
defaultTex = QuadPlainColor color3 1 0 0
boxTexSpec = BoxTextureSpec texes defaultTex


GraphObject型の要素は、StructureObjectの作成中にニーモニック関数を使用して構築されます。 現在、Structure.GraphObjectモジュールでは、primitiveBox、variableBox、functionBox、foundationBox、arrowBridgeBox、equalSignBridgeBox、bridgeBox、guardFrameの各関数を使用できます。 たとえば、いくつかの機能のみを提供します。

-モジュールStructure.GraphObject
プリミティブボックスtrans dim @ GL。Vector3 l h w texName = trans dim PrimitiveBox vertex3 l h w texName
variableBox _ dim @ GL。Vector3 l h w = nullVector3 dim PrimitiveBox vertex3 l h w helloTex

arrowBridgeBox dim = bridgeBox dim arrowTex
equalSignBridgeBox dim = bridgeBox dim equalSignTex

bridgeBox dim @ GL。Vector3 l h w texName =
nullVector3 dim TexturedBox vertex3 l h w boxTexSpec
どこで
boxTexSpec = BoxTextureSpec texes defTex
texes = [ SideTop QuadTexture texName ]
defTex = QuadTexture yellowBaseTex




また、レンダリングシステムについても説明する必要があります。 現時点では、グラフィックプリミティブをシーンオブジェクトにコンパイルし、すぐに描画しています。 これは、描画関数の一部として、プログラムループ内で永久に回転するレンダリング関数で発生します。 もちろん、このようなコードは非効率的です。なぜなら、同じStructureObjectツリーをコンパイルすると、オブジェクトを含む同じシーンが得られ、事前に準備できるからです。 ここに障害はありません。さらに、アクションのリスト[IO()]を返す場合、レンダリングとコンパイルは簡単に分離されます。

-Draw.Renderモジュール
texResをレンダリング StructureObject _ soTrans _ goSpecオブジェクト = do
GL soTransの変換-相対オフセットの設定
mapM_ texResをレンダリングオブジェクト-再帰的にツリーをたどる
sequence_ $ compileGraphObjectSpec texRes goSpec- プリミティブをコンパイルしてOpenGL呼び出しを行う
GL 翻訳する negateVector3 $ soTrans- 相対オフセットを削除します


レンダリングアルゴリズムは再帰的です。 ルートStructureObjectからツリーをたどって、子要素にますます多くのオフセットを設定し、前のレベルに戻ったら、これらのオフセットを削除します。 compileGraphObjectSpec関数は、GraphObjectをシーンオブジェクトにコンパイルします。 sequence_関数は、アクションのリスト[IO()]を実行します。 グラフィックオブジェクトには相対オフセットもあるため、アナログ座標の転送を行います。

-モジュールStructure.GOCompile
compileGraphObjectSpec texRes goTrans _、 go = let
forwardTrans = GL goTransを翻訳
コンパイル済み= compileGraphObject texRes go
backwardTrans = GL 翻訳する negateVector3 $ goTrans
in forwardTrans:compile ++ [ backwardTrans ]


一般に、Structure.GOCompileモジュールのコードは、使用される手法(たとえば、畳み込みやリストの内包表記)にとって非常に興味深いものです。 もちろん、そのようなコードを理解することはより困難ですが、私には、それ自体で価値のあるある種の内面の美しさと完全性があるように思えます:

-| 指定されたボックスの側面の図面のアクションを収集します。
-| このモジュールでのみ使用する必要があります。
f :: PreparedTextureObjects
-> GLfVertex3
-> BoxSide QuadColorSpec
-> [ BoxSide ] [ IO ]
-> [ BoxSide ] [ IO ]
f texRes boxDim side qColorSpec sList ioList = let
boxIO = do setQuadColorSpec texRes qColorSpec
GL renderPrimitive GL クワッド boxSide boxDim side
in side:sList boxIO:ioList

-| GraphObjectをコンパイルして、すぐに評価できるアクションリスト構造にします。 ([IO()])
compileGraphObject :: PreparedTextureObjects- > GraphObject- > [ IO ]

compileGraphObject _ NoGraphObject = [ ]

compileGraphObject texRes GraphObjects gObjectSpecs =
concatMap compileGraphObjectSpec texRes gObjectSpecs

compileGraphObject texRes PrimitiveBox boxDim texName =
[ GLを行います。 カラーcolorWhite
GL textureBinding GL Texture2D GL 。$ =ルックアップtexName texRes
GL renderPrimitive GL クワッド allBoxSides boxDim ]

compileGraphObject texRes TexturedBox boxDim boxTexSpec = let
BoxTextureSpec sideTexes defTex = boxTexSpec
textedSides textedSideDrawList = foldr f texRes boxDim [ ] [ ] sideTexes
untextedSides = [ s | s < -boxSideList s ` notElem` textedSides ]
untextedQColor = setQuadColorSpec texRes defTex
untextedSidesDraw = GL renderPrimitive GL クワッド boxSides boxDim untextedSides
intextedQColor:untextedSidesDraw:textedSideDrawList


ボックスは面で構成され、側面は色またはテクスチャで塗りつぶされています。 OpenGLでは、任意の形状を垂直に描画できます。 私たちの場合、これらは四角形の頂点になります。 頂点はdimに含まれる長さ、高さ、幅から計算され、ゼロ点はボックスの左下隅に対応します(単に、OpenGL座標系の軸がOX-右へ、OY-上へ、OZ-私たちへ) 。 フェイスは、HOpenGLライブラリの関数を使用してMisc.BoxSideモジュールで作成されます。

boxSide :: GLfVertex3- > BoxSide- > IO

boxSide GL。Vertex3 x y z SideTop = do
GL texCoord texCoordDR >> GL 頂点 vertex3 x y z
GL texCoord texCoordUR >> GL 頂点 vertex3 x y 0
GL texCoord texCoordUL >> GL 頂点 vertex3 0 y 0
GL texCoord texCoordDL >> GL 頂点 vertex3 0 y z

boxSide GL。Vertex3 x y z SideFront = do
GL texCoord texCoordUR >> GL 頂点 vertex3 x y z
GL texCoord texCoordUL >> GL 頂点 vertex3 0 y z
GL texCoord texCoordDL >> GL 頂点 vertex3 0 0 z
GL texCoord texCoordDR >> GL 頂点 vertex3 x 0 z

-... 6面すべてについてなど。

boxSideList = [ SideTop SideBottom SideLeft SideRight SideRear SideFront ]
boxSides boxDim = mapM_ boxSide boxDim
allBoxSides boxDim = boxSides boxDim boxSideList



さて、GraphServerプログラムの一般的な構造を表面的にトレースしました。 プログラムのサーバー部分の配置方法、ファイルからの写真のダウンロード方法、写真からのテクスチャの作成方法、サードパーティのユーティリティの使用方法については説明しません。 まだ多くの作業があり、視覚化サーバーの準備は約10%ですが、すべてのHaskell言語要素がスケッチの準備ができているわけではないため、作業は複雑です。 スクリーンショットで非常にはっきりと見える可視化には困難があります。 結局のところ、テクスチャのデバッグは理想とはほど遠いものです。 オブジェクト全体のテクスチャのストレッチを正規化し、装飾し、より調和のとれた何かを作成し、デザインに取り組みたいと思います。 それでもフォントと線画が必要です。 将来、アリティの関数の分析が追加されます。これは別のリファクタリング、他のアプローチ、他の方法です。 繰り返しますが、微妙なテクニック、巧妙なアルゴリズム、巧妙なデータ構造が必要になりますが、これらは簡単に開発することはできません。 私はみんなに彼に参加するよう勧めています。これはHaskellでの実践であり、大規模なプログラムの設計の経験、グラフィックスの知識、アルゴリズムの研究です。 しかし何よりも、私たちの生活をより美しくするのは芸術と創造性です。

コードは開いており、GitHubにあります: github.com/graninas/GraphServer

これはクロス記事です。 グラフィック言語の設計については、Haskell-Designの記事をお読みください。

PS読者へのリクエスト:プロジェクトへの参加に興味がある場合は、PM、ICQ、またはメールでご連絡ください。Googleでのメール。ニックネームはHabréと同じです。可能であれば、他のHaskellの愛好家の間で、またはプロジェクトに参加してそれを勉強したい人の間で記事を宣伝してください。集団開発では、プロジェクトとツールのいくつかの適応が必要になります。私はこの問題に専門的に取り組むことを約束します。

Source: https://habr.com/ru/post/J138884/


All Articles