
何かを視覚化するには、まずそれを意味のある単位に解析してから、グラフィック要素をマッピングする必要があります。 コードの場合、そのようなユニットは言語の構文要素になるため、最初のステップでコードを抽象構文ツリー(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の愛好家の間で、またはプロジェクトに参加してそれを勉強したい人の間で記事を宣伝してください。集団開発では、プロジェクトとツールのいくつかの適応が必要になります。私はこの問題に専門的に取り組むことを約束します。