Doom 3ソースコード分析

画像

2011年11月23日idソフトウェアは独自の伝統を維持し、以前のエンジンのソースコードを公開しました。

今回は、Prey、Quake 4、そしてもちろんDoom 3で使用されていたidTech4が登場しました 。わずか数時間で、GitHubに400を超えるリポジトリのフォークが作成され、人々はゲームの内部メカニズムを調べたり、他のプラットフォームに移植したりしました。 また、参加することに決め、Mac OS X用Intelバージョンを作成しました。JohnCarmackが親切に宣伝しました

クリーンさとコメントの観点から、これは、 Doom iPhoneコードベース(後でリリースされたため、コメントアウトしたほうがよい)以来のid Softwareコードの最高のリリースです。 このエンジンを研究し、組み立て、実験することを強くお勧めします。

ここにが理解したことに関するメモがあります。 いつものように、私はそれらをきれいにしました、彼らが数時間誰かを救って、そして彼らのプログラマースキルを改善するためにコードを学ぶように誰かを奨励することを望みます。

パート1:概要


コードベースを説明するために、ますます多くのイラストを使用し、テキストを減らしていることに気付きました。 以前はこのためにgliffyを使用していましたが、面倒な制限があります(たとえば、アルファチャネルの欠如)。 SVGとJavascriptに基づいて、3Dエンジンのイラスト専用に独自のツールを作成することを検討しています。 似たようなものが既にあるのでしょうか? まあ、コードに戻って...

はじめに


このような素晴らしいエンジンのソースコードにアクセスできることは非常に素晴らしいことです。 2004年のリリースの時点で、Doom IIIはリアルタイムエンジンの新しいグラフィックスとサウンドの標準を設定しました。その中で最も注目すべきものは、Unified Lighting and Shadowsでした。 テクノロジーにより、アーティストはハリウッドのファッションで自分自身を表現できるようになりました。 8年後でも、Delta-Labs-4でのHellKnightとの最初の会議は依然として信じられないほど壮観です。


最初の連絡


ソースコードはGithubを介して配布されるようになりました。これは、IDソフトウェアFTPサーバーがほぼ常に置かれているか、過負荷になっていたためです。



オリジナルの TTimo リリースは 、Visual Studio 2010 Professionalを使用して正常にコンパイルされます。 残念ながら、Visual Studio 2010 "Express"にはMFCがないため、使用できません。 リリース後、これは少し残念ですが、 依存関係は削除されました

Windows 7 :
===========



git clone https://github.com/TTimo/doom3.gpl.git




コードの読み取りと調査には、Mac OS XでXcode 4.0を使用することを好みます。SpotLightからの検索速度、変数の強調表示、適切な場所に移動するための「コマンドクリック」により、Visual Studioよりも作業が便利になります。 Xcodeプロジェクトはリリース中に破損しましたが、修正するの非常に簡単であり、Mac OS X Lionで正常に動作するユーザー「不良セクター」のGithubリポジトリがあります。

MacOS X :
=========



git clone https://github.com/badsector/Doom3-for-MacOSX-


注: Visual Studio 2010 Productivity Power Toolsをインストールした後、Visual Studio 2010で変数を強調表示して「Control-Click」をクリックすることもできます 。 これが「バニラ」インストールパッケージに含まれていない理由がわかりません。

両方のコードベースが最適な状態になりました。1回クリックするだけで実行可能ファイルをビルドできます。


興味深い事実:ゲームを開始するには、Doom 3リソースを含むbaseフォルダーが必要です。Doom3 CDからそれらを抽出して更新するのに時間を無駄にしたくなかったので、Steamからバージョンをダウンロードしました。 公開されたVisual Studioプロジェクトのデバッグ設定に"+set fs_basepath C:\Program Files (x86)\Steam\steamapps\common\doom 3"がまだあるため、id Softwareの人たちも同じことをしたようです!

興味深い事実:このエンジンはVisual Studio .NET( ソース )で開発されました。 ただし、コード内のC#には単一行がなく、コンパイル用に公開されたバージョンにはVisual Studio 2010 Professionalが必要です。

興味深い事実: Id SoftwareチームはMatrixフランチャイズのファンであるようです。QuakeIIIのワーキングネームは「Trinity」で、Doom IIIのワーキングネームは「Neo」です。 これは、すべてのソースコードがneoサブフォルダーにある理由を説明しています。

建築


ゲームは、エンジンの全体的なアーキテクチャを反映するプロジェクトに分割されます。

プロジェクト
組立
注釈

Mac OS X
ゲーム
gamex86.dll
gamex86.so
Doom3ゲームプレイ
Game-d3xp
gamex86.dll
gamex86.so
ゲームプレイDoom3 eXPension(Ressurection)
MayaImport
MayaImport.dll
-リソース作成ツールチェーンの一部:Mayaファイルを開き、モンスター、カメラルート、マップをインポートするために実行時にロードされます。
運命3
Doom3.exe
Doom3.app
ドゥーム3エンジン
Typeinfo
TypeInfo.exe
-RTTI内部ヘルパーファイル: GameTypeInfo.h生成します。 GameTypeInfo.hは、すべてのタイプのDoom3クラスと各要素のサイズのマップです。 これにより、TypeInfoクラスを使用してメモリをデバッグできます。
カールリブ
Curllib.lib
-ファイルのダウンロードに使用されるHTTPクライアント(gamex86.dllおよびdoom3.exeに静的にリンク)。
idLib
idLib.lib
idLib.a
IDソフトウェアライブラリ。 パーサー、レキシカルアナライザー、辞書...(静的にgamex86.dllおよびdoom3.exeにリンク)が含まれています。

idTech2から始まる他のすべてのエンジンと同様に、1つのクローズドソースバイナリファイル(doom.exe)と1つのオープンソースダイナミックライブラリ(gamex86.dll)が表示されます。



コードベースのほとんどは、2004年10月以降Doom3 SDKで利用可能になりましたが、Doom3実行可能ファイルのソースコードのみが欠落していました。 Modderはidlib.aおよびgamex86.dllビルドできましたが、エンジンコアはまだ閉じられていました。

注:エンジンは標準C ++ライブラリを使用しません。すべてのコンテナー(マップ、ポインター付きリスト...)は新たに実装されますが、 libcが積極的に使用されます。

注: Gameモジュールでは、各クラスはidClassを継承します。 これにより、エンジンは内部RTTIを実行し、クラス名でクラスをインスタンス化できます。

興味深い事実:この図を見ると、必要なフレームワーク( Filesystemなど)の一部がDoom3.exeプロジェクトにあることがわかります。 gamex86.dllもリソースをロードする必要があるため、これは問題を引き起こします。 これらのサブシステムは、doom3.exeからgamex86.dllライブラリによって動的にロードされます(これが図の矢印の意味です)。 PEエクスプローラーでDLLを開くと、gamex86.dllが1つのメソッドGetGameAPIをエクスポートしていることがわかります。



オブジェクトにポインターを渡すことで、Quake2がレンダラーとゲームのddlをロードしたのとまったく同じように機能します。

Doom3.exeがロードされると、次のようになります。


  gameExport_t * GetGameAPI_t( gameImport_t *import ); 

この「接続セットアップ」Doom3.exeの最後には、 idGameオブジェクトへのポインターがあり、Game.dllには、欠落しているすべてのサブシステムへの追加リンク(例: idFileSystemを含むgameImport_tオブジェクトへのポインターがあります。

Gamex86がDoom 3実行可能オブジェクトを認識する方法:

  typedef struct { int version; //  API idSys * sys; //    idCommon * common; //  idCmdSystem * cmdSystem //    idCVarSystem * cvarSystem; //    idFileSystem * fileSystem; //   idNetworkSystem * networkSystem; //   idRenderSystem * renderSystem; //   idSoundSystem * soundSystem; //   idRenderModelManager * renderModelManager; //    idUserInterfaceManager * uiManager; //    idDeclManager * declManager; //   idAASFileManager * AASFileManager; //   AAS idCollisionModelManager * collisionModelManager; //    } gameImport_t; 

Doom 3がGame / Moddオブジェクトを認識する方法:

  typedef struct { int version; //  API idGame * game; //     idGameEdit * gameEdit; //     } gameExport_t; 

注:各サブシステムをよりよく理解するための優れたリソースは、 Doom3 SDKドキュメントページです。2004年にコードを深く理解した人(つまり、開発チームの1人)によって書かれたようです。

コード


解析する前に、 clocからの統計を以下に示します。

./cloc-1.56.pl neo

2180 text files.
2002 unique files.
626 files ignored.

http://cloc.sourceforge.net v 1.56 T=19.0 s (77.9 files/s, 47576.6 lines/s)

-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
C++ 517 87078 113107 366433
C/C++ Header 617 29833 27176 111105
C 171 11408 15566 53540
Bourne Shell 29 5399 6516 39966
make 43 1196 874 9121
m4 10 1079 232 9025
HTML 55 391 76 4142
Objective C++ 6 709 656 2606
Perl 10 523 411 2380
yacc 1 95 97 912
Python 10 108 182 895
Objective C 1 145 20 768
DOS Batch 5 0 0 61
Teamcenter def 4 3 0 51
Lisp 1 5 20 25
awk 1 2 1 17
-------------------------------------------------------------------------------
SUM: 1481 137974 164934 601047
-------------------------------------------------------------------------------

通常、コードの行数で明確なことを言うことはできませんが、ここではエンジンを理解するために必要な作業を評価するのに非常に役立ちます。 コードには601,047行あります。つまり、エンジンはQuake IIIの2倍「理解」が困難です。 コードの行数におけるidソフトウェアエンジンの履歴に関する統計:
コードの行運命idTech1idTech2idTech3idTech4
エンジン39079143855135788239398601032
ツール3411115528140128417-
合計39420155010163928367815601032



注: idTech3のボリュームの大幅な増加は、lccコードベースのツールによるものでした(Cコンパイラーを使用してQVMバイトコードを生成しました)。

注: Doom3の場合、ツールはエンジンコードベースに含まれているため、考慮されません。

高レベルでは、いくつかの面白い事実に気付くことができます。


John Carmackによって書かれたidTech4 Code Writing StandardMirror )を見るのも興味深いです(特に、 constの場所に関するコメントに感謝します)。

サイクルを拡大する


エンジンの最も重要な部分でのメインサイクルの分析は次のとおりです。

  idCommonLocal commonLocal; //    idCommon * common = &commonLocal; //   ( Init   ,   ) int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { Sys_SetPhysicalWorkMemory( 192 << 20, 1024 << 20 ); //Min = 201,326,592 Max = 1,073,741,824 Sys_CreateConsole(); //   ,   :     "" ( )  . for (int i = 0; i < MAX_CRITICAL_SECTIONS; i++ ) { InitializeCriticalSection( &win32.criticalSections[i] ); } common->Init( 0, NULL, lpCmdLine ); //    VRAM (   OpenGL,   ) Sys_StartAsyncThread(){ //     . while ( 1 ){ usleep( 16666 ); //   60  common->Async(); //   Sys_TriggerEvent( TRIGGER_EVENT_ONE ); //   ,   pthread_testcancel(); // ,       (  ). } } Sys_ShowConsole while( 1 ){ Win_Frame(); // /  common->Frame(){ session->Frame() //   { for (int i = 0 ; i < gameTicsToRun ; i++ ) RunGameTic(){ game->RunFrame( &cmd ); //         GameX86.dll. for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) ent->GetPhysics()->UpdateTime( time ); //    } } session->UpdateScreen( false ); //     { renderSystem->BeginFrame idGame::Draw //  .     ! renderSystem->EndFrame R_IssueRenderCommands //  .     . } } } } 

完全に分解されたサイクルの詳細については、 こちらをご覧ください 。 コードを読むとき、私はそれをマップとして使用しました。

これはidソフトウェアエンジンの標準メインループです。 Sys_StartAsyncThread除きます。これは、Doom3がマルチスレッドであることを意味します。 このストリームの目的は、エンジンがフレームレートによって制限してはならないタイムクリティカルな機能を管理することです。


興味深い事実:すべてのidTech4トップレベルオブジェクトは仮想メソッドを持つ抽象クラスです。 通常、これはパフォーマンスを低下させます。各仮想メソッドのアドレスは、実行時に呼び出す前にvtableで見つける必要があるためです。 しかし、これを回避する「トリック」があります。 すべてのオブジェクトのインスタンスは、次のように静的に作成されます。

  idCommonLocal commonLocal; //  idCommon * common = &commonLocal; //   gamex86.dll 

データセグメントに静的に配置されたオブジェクトは既知の型であるため、コンパイラはcommonLocalメソッドを呼び出すときにvtableの検索を最適化できます。 接続(ハンドシェイク)を確立するとき、インターフェイスポインターが使用されるため、 doom3.exeはオブジェクト参照をgamex86.dllと交換できますが、この場合、vtableでの検索のコストは最適化されません。

興味深い事実: id Softwareエンジンのほとんどを研究した結果、doom1エンジン以来、メソッド名が変更されていないことは注目に値します。マウス入力とジョイスティックを読み取るメソッドはIN_frame()です。

レンダラー


2つの重要な部分:


プロファイリング


XcodeのInstrumentsを使用して、CPUサイクルが何をしているかを確認しました。 結果と分析については、以下の「プロファイリング」セクションを参照してください。

スクリプトと仮想マシン


各製品では、idTech VMとスクリプト言語が完全に変更されました...そしてidが再度変更しました(詳細については、「スクリプトVM」のセクションを参照してください)

インタビュー


コードを読んでいる間、私はいくつかの革新に戸惑っていたので、ジョン・カーマックに手紙を書きました。


さらに、私は1ページにすべてのビデオとidTech4についてのマスコミとのインタビューを集めました。 それらはすべてインタビューページで収集されます

パート2:Dmap


すべてのid Softwareエンジンと同様に、設計チームによって作成されたカードは、ユーティリティによる強力な予備処理を受けて、実行時のパフォーマンスを向上させます。

idTech4では、このユーティリティはdmapと呼ばれ、その目的は.mapファイルから多面体からスープを読み取り、ポータルによって接続された領域を特定し、 .procファイルに保存することです。

このツールの目的は、 doom3.exeランタイムポータルシステムを使用することdoom3.exe 。 Seth Tellerによる1992年の驚くべき記事があります「密集した多面体環境での可視性の計算」 idTech4エンジンがどのように機能するかについて、多くの図とともに詳細に説明します。

エディター


デザイナーは、CSG(Constructive Solid Geometry)を使用してレベルマップを作成します。通常、6つの面を持つ多面体を使用して、マップ上に配置します。

これらのブロックは「ブラシ」と呼ばれます。 次の図では8つのブラシが使用されています(同じマップを使用して各dmapステップを説明します)。

デザイナーは「内部」(最初の図)をよく理解しているかもしれませんが、 dmapはブラシからスープのみを受け取り、内部または外部には何もありません(2番目の図)。

デザイナーが見るもの.mapファイルからブラシを読み取るときにDmapが表示するもの。





ブラシは、面ではなく、平面を通して定義されます。 面の代わりに平面を定義するのは非効率に思えるかもしれませんが、後で2つのサーフェスが同じ平面上にあるかどうかを確認するときに非常に役立ちます。 プレーンは「同じように」方向付けられていないため、内部または外部の部品はありません。 平面の向きは、ボリュームの内部と外部で異なる場合があります。

コードレビュー


Dmapのソースコードは非常によくコメントアウトされています。その番号を見てください。コードよりもコメントが多いです!

  bool ProcessModel( uEntity_t *e, bool floodFill ) { bspface_t *faces; //  bsp-     //    faces = MakeStructuralBspFaceList ( e->primitives ); e->tree = FaceBSP( faces ); //      , //     MakeTreePortals( e->tree ); //        FilterBrushesIntoTree( e ); // ,   bsp  if ( floodFill && !dmapGlobals.noFlood ) { if ( FloodEntities( e->tree ) ) { //     FillOutside( e ); } else { common->Printf ( "**********************\n" ); common->Warning( "******* leaked *******" ); common->Printf ( "**********************\n" ); LeakFile( e->tree ); //     // "" ,    // -noFlood return false; } } //         //        , //         ClipSidesByTree( e ); //  ,     //  ,        FloodAreas( e ); //     BSP-     ,   //      ,  //     PutPrimitivesInAreas( e ); //         //      , //        //  Prelight( e ); //  -    T-  if ( !dmapGlobals.noOptimize ) { OptimizeEntity( e ); } else if ( !dmapGlobals.noTJunc ) { FixEntityTjunctions( e ); } //   -    FixGlobalTjunctions( e ); return true; } 

0.レベルジオメトリの読み込み


.mapファイルは、エンティティのリストです。 レベルは、worldspawnクラスを持つファイル内の最初のエンティティです。 エンティティには、ほとんど常にブラシであるプリミティブのリストが含まれています。 残りのエンティティは、光源、モンスター、プレイヤーのスポーンポイント、武器などです。

  Version 2 //  0 { "classname" "worldspawn" //  0 { brushDef3 { ( 0 0 -1 -272 ) ( ( 0.0078125 0 -8.5 ) ( 0 0.03125 -16 ) ) "textures/base_wall/stelabwafer1" 0 0 0 ( 0 0 1 -56 ) ( ( 0.0078125 0 -8.5 ) ( 0 0.03125 16 ) ) "textures/base_wall/stelabwafer1" 0 0 0 ( 0 -1 0 -3776) ( ( 0.0078125 0 4 ) ( 0 0.03125 0 ) ) "textures/base_wall/stelabwafer1" 0 0 0 ( -1 0 0 192 ) ( ( 0.0078125 0 8.5 ) ( 0 0.03125 0 ) ) "textures/base_wall/stelabwafer1" 0 0 0 ( 0 1 0 3712 ) ( ( 0.006944 0 4.7 ) ( 0 0.034 1.90) ) "textures/base_wall/stelabwafer1" 0 0 0 ( 1 0 0 -560 ) ( ( 0.0078125 0 -4 ) ( 0 0.03125 0 ) ) "textures/base_wall/stelabwafer1" 0 0 0 } } //  1 { brushDef3 } //  2 { brushDef3 } } . . . //  37 { "classname" "light" "name" "light_51585" "origin" "48 1972 -52" "texture" "lights/round_sin" "_color" "0.55 0.06 0.01" "light_radius" "32 32 32" "light_center" "1 3 -1" } 

各ブラシは複数のプレーンとして記述されます。 ブラシの側面は面(またはベンド)と呼ばれ、各面はブラシの他のすべての面で面をトリミングすることによって取得されます。

注:読み込み段階では、非常に興味深い高速のプレーンハッシングシステムが使用されます(Plane Hashing System): idHashIndex idPlaneSet上に作成され、一見の価値があります。

1. MakeStructuralBspFaceListおよびFaceBSP


最初のステップは、バイナリスペースパーティションを使用してマップをカットすることです。 マップの各不透明な面は、分離面として使用されます。

次のセパレータ選択ヒューリスティックが使用されます。

1:マップに5,000ユニット以上ある場合:スペースの中央で軸指向平面(軸整列平面)を使用してカットします。 以下の画像では、6000x6000のスペースが3回カットされています。



2: 5000ユニットを超えるパーツが残っていない場合 「ポータル」とマークされた面を使用します(マテリアルtextures/editor/visportal )。 以下の図では、ポータルブラシは青でマークされています。



3:残りのエッジを使用します。 他のほとんどと同一線上にある面を選択し、最小の面をカットします。 軸スペーサも好ましい。 分離面は赤でマークされています。





使用可能な面がない場合、プロセスは終了します。BSPツリーのシート全体が凸部分空間です。



2. MakeTreePortals


現在、マップは凸部分空間に分割されていますが、これらの部分空間はお互いについて何も知りません。 このステップの目標は、ポータルを自動的に作成することにより、各リーフをその隣接ノードに接続することです。 アイデアは、マップを制限する6つのポータルから始めることです。「外部」を「内部」に(BSPのルートで)接続します。 次に、BSPの各ノードについて、ノード内の各ポータルを分割し、分離面をポータルとして追加し、再帰的に繰り返します。





6つのソースポータルは分割され、リーフに展開されます。ノードが分割されるたびに、これに接続されている各ポータルも分割する必要があるため、これは見かけほど簡単ではありません。

左の図では、1つのポータルがBSPツリー内の2つの「兄弟」ノードを接続しています。左の子シートをたどると、その分割面はポータルを2つに分割します。他のノードのポータルも更新して、「兄弟」や「ne」に接続しないようにする必要があることがわかります。

プロセスの最後に、6つのソースポータルが数百のポータルに分割され、新しいポータルが分離面に作成されます。BSPの

各シートは、共通のエッジを持つリーフに接続するポータルのリンクリストのおかげで、隣接を認識します:



3. FilterBrushesIntoTree




このステージは、BSPがボードで、ブラシがシェイプであるシェイプの選択がある子供向けゲームに似ています。不透明なを検出するために、各ブラシがBSPに送られます。

この方法は、よく説明されたヒューリスティックのおかげで機能します。ブラシが分離平面をわずかに横切るが、EPSILONを超えない場合、代わりに、ブラシの他のすべての要素が配置されている平面の側面に完全に行きます。

これで、「内部」部分と「外部」部分が表示され始めます。

ブラシが接触したシートは不透明(固体)と見なされ、それに応じてマークが付けられます。



4. FloodEntitiesおよびFillOutside


プレイヤーのスポーンエンティティを使用して、塗りつぶしアルゴリズムが各シートに対して実行されます。彼は、葉がエンティティから到達可能であるとマークします。



FillOutsideの最終段階は各シートを通過し、到達できない場合は不透明としてマークします。



各サブスペースが達成可能または不透明なレベルを取得しました。リーフポータルを介したナビゲーションは均一になり、ターゲットリーフの不透明度をチェックすることで実行されます。

5. ClipSidesByTree


ブラシの不要な部分を破棄する時が来ました。ブラシの各ソース側がBSPを下っていきます。側面が不透明なスペース内にある場合、破棄されます。それ以外の場合は、visibleHull関係者のリストに追加されます。

その結果、「スキン」レベルが取得され、可視部分のみが保持されます。



これ以降、残りの操作については、visibleHull関係者のリストのみが考慮されます。

6. FloodAreas


dmapのグループの葉の識別子エリア:各シートの塗りつぶしアルゴリズムのため。彼は、シートに関連付けられているポータルを通じてすべてを埋めようとします。

ここで、デザイナーの作業は非常に重要です。エリアは、ポータル(手順1で言及したポータルのブラシ)を手動でマップ上に配置した場合にのみ識別できます。それらがなければ、それdmapは1つの領域のみを識別し、ビデオプロセッサの各フレームはカード全体に送信されます。

再帰塗りつぶしアルゴリズムは、エリアポータルと不透明ノードによってのみ停止されます。以下の図では、自動生成されたポータル(赤)は引き続き塗りつぶされますが、デザイナーによって配置されたvisportal(青、areaportalとも呼ばれます)は、2つの領域を作成することで停止します。





プロセスの最後に、各非連続シートはリージョンに属し、リージョン間ポータル(青)が定義されます。



7. PutPrimitivesInAreas


この段階で、別のゲーム「形状を見つける」で、ステップ6で定義された領域とステップ5で計算されたvisibleHullが結合されます。今回はボードが領域であり、ピースがvisibleHullです。

領域の配列が選択され、各ブラシの各visibleHullがBSPに送信されます。インデックスareaIDにより、表面が領域の配列に追加されます。

注:かなりスマートな動き-この段階では、エンティティの生成も最適化されます。一部のエンティティが「func_static」としてマークされている場合、それらのインスタンスはすぐに作成され、エリアにバインドされます。したがって、ボックス、樽、椅子をその領域に「接着」することができます(それらの影を事前に生成することによっても)。

8.プリライト


静的光源ごとdmapに、影のボリュームのジオメトリを事前に計算します。これらのボリュームは後で保存されます.proc唯一の秘isは"_prelight_light"、エンジンがファイルの光源.mapファイルの影のボリュームを一致させることができるように、影のボリュームが光源の識別子に接続された名前で保存されることです.proc

  shadowModel { /* name = */ "_prelight_light_2900" /* numVerts = */ 24 /* noCaps = */ 72 /* noFrontCaps = */ 84 /* numIndexes = */ 96 /* planeBits = */ 5 ( -1008 976 183.125 ) ( -1008 976 183.125 ) ( -1013.34375 976 184 ) ( -1013.34375 976 184 ) ( -1010 978 184 ) ( -1008 976 184 ) ( -1013.34375 976 168 ) ( -1013.34375 976 168 ) ( -1008 976 168.875 ) ( -1008 976 168.875 ) ( -1010 978 168 ) ( -1008 976 167.3043518066 ) ( -1008 976 183.125 ) ( -1008 976 183.125 ) ( -1010 978 184 ) ( -1008 976 184 ) ( -1008 981.34375 184 ) ( -1008 981.34375 184 ) ( -1008 981.34375 168 ) ( -1008 981.34375 168 ) ( -1010 978 168 ) ( -1008 976 167.3043518066 ) ( -1008 976 168.875 ) ( -1008 976 168.875 ) 4 0 1 4 1 5 2 4 3 4 5 3 0 2 1 2 3 1 8 10 11 8 11 9 6 8 7 8 9 7 10 6 7 10 7 11 14 13 12 14 15 13 16 12 13 16 13 17 14 16 15 16 17 15 22 21 20 22 23 21 22 18 19 22 19 23 18 20 21 18 21 19 1 3 5 7 9 11 13 15 17 19 21 23 4 2 0 10 8 6 16 14 12 22 20 18 } 

9. FixGlobalTjunctions


Tジョイントの修正は、通常、視覚的なアーティファクトを取り除くために重要ですが、idTech4ではさらに重要です。テンプレートバッファーへの書き込み時にジオメトリを使用してシャドウを生成することもできます。Tジョイントには2つの問題があります。

10.データ出力


最後に、前処理されたすべてのデータがファイルに保存されます.proc


物語


コードの多くのセグメントは、Quake()、Quake 2()、およびQuake 3()の前処理ツールで使用されるコードにdmap似ていますこの理由は、潜在的な可視セット(PVS)が一時的なポータルシステムを使用して生成されるためです。qbsp.exeq2bsp.exeq3bsp.exe


図は常に優れています。たとえば、qbsp.exeポータルで接続された6つのリーフ見つけvis.exePVSを生成するために実行されるようになりました。このプロセスはシートごとに実行されますが、この例ではシート1のみを考慮します。



シートは常にそれ自体から見えるため、シート1の初期PVSは次のようになります。
シートID123456
ビットベクトル(シート1のPVS)1

充填アルゴリズムが開始されます。ルールには、パスに2つのポータルはないが、シートは開始点から見えると見なされます。これは、次のPVSでシート3に到達することを意味します。

シートID123456
ビットベクトル(シート1のPVS)111



シート3では、実際に可視性を確認できます。ポータルn-2とポータルn-1から2つのポイントを取得して、クリッピングプレーンを生成し、潜在的な可視性について次のポータルをテストできます。

図から、リーフ4と6につながるポータルはテストに合格せず、シート5へのポータルは合格することがわかります。次に、シート6のフィルアルゴリズムが再帰的に実行され、シート1のPVSの終了時に次のようになります。

シートID123456
ビットベクトル(シート1のPVS)111010

IdTech4はPVSを生成せず、代わりにポータルデータが保存されます。各エリアの可視性は、投影時にポータルを画面スペースに曲げて、互いに対してトリミングすることにより計算されます。

興味深い事実: Michael Abrashは、GDC Vaultからのこの素晴らしいビデオのマーカーを使用して、10秒でプロセス全体を説明しました(写真はクリック可能です):



パート3:レンダラー


idTech4レンダラーは、3つの重要な革新を行いました。


idTech4で最も重要なのはマルチパスレンダラーです。視界内の各光源の影響は、加算混合を使用してビデオプロセッサのフレームバッファに蓄積されます。 Doom 3は、フレームバッファーのカラーレジスタが転送ではなく飽和を実行するという事実を最大限に活用します。加算ミキシングを説明するために、独自のレベルを作成しました。以下のスクリーンショットは、3つのパスが実行される3つの光源を示しています。各パスの結果はフレームバッファに蓄積されます。すべての光源が混合されている画面中央の白色光に注目してください。各通路を分離するようにエンジンを変更しました:通路1:青色光源通路2:緑色光源通路3:赤色光源

() :
============================

1111 1111
+ 0000 0100
---------
= 0000 0011

() :
==========================

1111 1111
+ 0000 0100
---------
= 1111 1111

















エンジンに他の変更を加えて、光源の各パスの後にフレームバッファーの状態を確認しました:


最初のパス


後のビデオプロセッサーバッファー2番目のパス


のビデオプロセッサーフレームバッファー3番目のパスの後のビデオプロセッサーフレームバッファー

興味深い事実:光源の各パスの結果を取得し、それらを手動で混合することができますPhotoshop(OpenGL加算ミキシングをシミュレートする線形回避)とまったく同じ結果を取得します。

シャドウのサポートとバンプマッピングを組み合わせたアディティブブレンドは、2012年の基準でも、エンジンに非常に優れた画像を作成します。



建築


以前のidTechエンジンとは異なり、レンダラーはモノリシックではありませんが、2つの部分(フロントエンドとバックエンド)に分割されています。




レンダラーのアーキテクチャは、Quake3仮想マシンのバイトコードを生成するために使用されるLCCクロスコンパイラーに非常に似ています



当初、レンダラーの設計はLCCの設計に影響されると考えましたが、SMPシステムでマルチスレッドなるため、レンダラーは2つの部分に分割されました。フロントエンドは1つのコアで実行され、バックエンドは別のコアで実行されることになっています。残念ながら、一部のドライバーは不安定であるため、別のスレッドを無効にする必要があり、両方の部分が同じスレッドで実行されました。

起源に関する興味深い事実:コードを使用すると、考古学的な調査を実行することもできます-レンダラーの詳細なコードをよく見ると、エンジンがC ++からC(オブジェクトから静的メソッド)に移行していることが明確にわかります。

これはコード履歴が原因で発生しました。idTech4レンダラーは、Quake3エンジン(Cコードベース)に基づいてJohn CarmackがC ++スペシャリストになるまで作成しました。レンダラーは後にC ++のidtech4コードベースに統合されました。

Doom3にはQuakeがどれだけ残っていますか?言うのは難しいですが、Mac OS Xの主な方法は次のとおりです。

  - (void)quakeMain; 

フロントエンド、バックエンド、ビデオプロセッサとの相互作用


この図は、フロントエンド、バックエンド、およびビデオプロセッサ間の相互作用を示しています。



  1. フロントエンドは世界の状態を分析し、2つの結果を生成します。
    • 視野に影響を与える各光源のリストを含む中間ビュー。各光源には、それと相互作用するエンティティサーフェスのリストが含まれています。
    • , , . VBO .
  2. . OpenGL , . VBO .
  3. OpenGL .

Doom3


フロントエンドは困難なタスクを実行します:可視サーフェスの定義(可視サーフェス決定、VSD)。その目的は、視野に影響を与える光源とエンティティのすべての組み合わせを見つけることです。このような組み合わせは、相互作用と呼ばれます。すべてのインタラクションが見つかると、フロントエンドはバックエンドが必要とするすべてをビデオプロセッサのRAMにロードします(「インタラクションテーブル」を使用してすべてを追跡します)。最後のステップは、OpenGLコマンドを生成するためにバックエンドによって読み取られる中間ビューを生成することです。

コードでは次のようになります。

  - idCommon::Frame - idSession::UpdateScreen - idSession::Draw - idGame::Draw - idPlayerView::RenderPlayerView - idPlayerView::SingleView - idRenderWorld::RenderScene - build params - ::R_RenderView(params) //   { R_SetViewMatrix R_SetupViewFrustum R_SetupProjection //    . static_cast<idRenderWorldLocal *>(parms->renderWorld)->FindViewLightsAndEntities() { PointInArea //  BSP     FlowViewThroughPortals //   ,      ,    . } R_ConstrainViewFrustum //   Z-      . R_AddLightSurfaces //  ,     ,    (  ) R_AddModelSurfaces //     ( ) R_RemoveUnecessaryViewLights R_SortDrawSurfs //   qsort  C.     C++   . R_GenerateSubViews R_AddDrawViewCmd } 

注: CからC ++への移行はここでは明らかです。

例を理解するのは常に簡単なので、ここにレベルがあります。配設されたデザイナーのvisplanesエンジンは、次の4つの領域を見ている:



ロードすると.procエンジンはまた、アップロードされた.mapすべての光源を決定し、実体を移動備えます。エンジンは、光源ごとに、影響を受けるすべての領域のリストを作成しました。実行時に、プレイヤーの位置とモンスターが影を落とすようになりました。シーンを正確にするには、すべてのモンスターと影を見つける必要があります。プロセスは次のとおりです。



1 :
=========

- 0
- 1

2 :
=========

- 1
- 2
- 3








  1. プレーヤーがBSPツリーをバイパスしているエリアを見つけますPointInArea
  2. FlowViewThroughPortals : . . Realtime rendering :



    , , , :

    ( /) :
    ==================================

    1 - 0
    1 - 1
    1 - 1

    2 - 1
    2 - 1


    : « 2 — 2», 2 .
  3. R_AddLightSurfaces , , .

    ( /) :
    ==================================

    1 - 0
    1 - 1
    1 - 1

    2 - 1
    2 - 1
    2 - 2
  4. R_AddModelSurfaces:すべてのインタラクションが見つかりました。頂点とインデックスがまだない場合は、ビデオプロセッサのVBOに読み込みます。これにより、アニメーションモンスターのジオメトリのインスタンス(モデルとシャドウのボリューム)が作成されます。
  5. すべての「知的」作業が完了しました。ヘルプを使用して、バックエンドに画面へのレンダリングを開始R_AddDrawViewCmdせるコマンドが発行されRC_DRAW_VIEWます。

Doom3バックエンドレンダラー


バックエンドは、ビデオプロセッサの制限を考慮して、中間ビューをレンダリングします。Doom3は、ビデオプロセッサをレンダリングする5つの方法をサポートしました。


2012年には、ARB2のみが最新のビデオプロセッサでサポートされています。この規格は、移植性を提供するだけでなく、ゲームの寿命を延ばしました。

ビデオカードがバンプマッピング(数年前に書いたHellknightの使用方法に関するチュートリアル)とリフレクションマップをサポートしている場合、idtech4はそれらをオンにしましたが次の操作でピクセルフィルレートの保存に全力を尽くしました


詳細なバックエンドコードは次のとおりです。

  idRenderSystemLocal::EndFrame R_IssueRenderCommands RB_ExecuteBackEndCommands RB_DrawView RB_ShowOverdraw RB_STD_DrawView { RB_BeginDrawingView //  z-,     .. RB_DetermineLightScale RB_STD_FillDepthBuffer //      ( )  . //        ,      _DrawInteractions { 5 GPU specific path switch (renderer) { R10 (GeForce256) R20 (geForce3) R200 (Radeon 8500) ARB (OpenGL 1.X) ARB2 (OpenGL 2.0) } //     qglStencilFunc( GL_ALWAYS, 128, 255 ); RB_STD_LightScale //        (, ,  ....) int processed = RB_STD_DrawShaderPasses( drawSurfs, numDrawSurfs ) //      RB_STD_FogAllLights(); //       _currentRender if ( processed < numDrawSurfs ) RB_STD_DrawShaderPasses( drawSurfs+processed, numDrawSurfs-processed ); } 

バックエンドステージを段階的に進めるために、有名な画面をDoom3レベル



から取得し、視覚化の各段階でエンジンを停止しました.Doom3は拡散テクスチャの上にバンプマッピングと反射マップを使用するため、サーフェスレンダリングでは3つのテクスチャで検索を使用できます。ピクセルは5〜7個の光源の影響を受ける可能性があるため、再描画を行わなくても、ピクセルあたり最大21個のテクスチャ検索の可能性を想定するのは狂気ではありません。再描画を0にするには、バックエンドの最初の段階が必要です。すべてのシェーダーを無効にし、深度バッファーのみに書き込み、すべてのジオメトリをレンダリングします:



深度バッファーがいっぱいになりました。この時点から、深度記録は無効になり、深度テストがオンになります。

主にzバッファへのレンダリングは逆効果のように思えるかもしれませんが、実際には、フィルレートの保存に非常に役立ちます。


カラーバッファはクリーニングされ、黒で塗りつぶされていることに注意してください。自然な形では、Doom3の世界は完全に黒です。「アンビエント」ライティングがないためです-表示するには、ポリゴン/サーフェスが光源と相互作用する必要があります。これが、Doom3が非常に暗い理由です。

その後、エンジンは11パスを実行します(各光源に1つ。

レンダリングプロセスを部分に分割しました。以下の図は、光源の個々の通過を示しています。


光源の


効果 1 光源の


効果 2 光源の


効果 3 光源の


効果 4光源の効果5


光源の


効果 6光源の効果7


光源の 効果8光源の


効果9


光源の


効果 10光源の効果11


ラストパス:環境光の通過

そして、ビデオプロセッサのフレームバッファーで何が起こるか:


光源


の通過後 1 光源


の通過後 2 光源


の通過後 3 光源


の通過後 4光源の通過後5


光源6を通過した後、


光源7を通過後に


光源8を通過後に


光源9を通過後に


光源10を通過した後


、光源11の通過後に


光源12の通過後に


R光源13が通過する次

のテンプレートは、テストバッファとはさみ:

光源によって影が落とされる場合、各光源が通過する前にテンプレートテストを実行する必要があります。深度失敗/深度パスの矛盾と悪名高いCreative Labsの動きについては詳しく説明しません。公開されたソースコードでは、高品質のシャドウを構築する必要があるため、深度パスアルゴリズムが遅くなります。誰かが深さ失敗アルゴリズムをソースコードに返すことができましたが、これはヨーロッパでのみ合法であることに注意してください!

充填率を節約するために、フロントエンドは、OpenGLのシザーテストに使用する画面スペースの長方形を生成します。これにより、光源までの距離が原因で表面が黒のままになるピクセルのシェーダーが不要になります。

テンプレートバッファは、光源8が通過する直前に適用されます。黒以外の領域はすべて塗りつぶされます。他の領域では、フレームバッファへの書き込みが制限されます。



テンプレートバッファーは、光の通路7の直前にあります。フィルレートを節約するために、はっきりと見えるハサミです。

<img src = " fd.fabiensanglard.net/doom3/renderer/DOOM3-Context3-Static-StencilBuffer2.png "

インタラクティブな表面


レンダリングの最終段階は次のRB_STD_DrawShaderPassesとおりです。光を必要としないすべてのサーフェスをレンダリングします。これらには、画面とグラフィカルユーザーインターフェイスの見事なインタラクティブサーフェスが含まれます。エンジンのこの部分、ジョン・カーマックは最も誇りに思っていました。彼女は彼女が負っているすべての尊敬を得たとは思わない。2004年、最初のスプラッシュスクリーンは通常フルスクリーンビデオでした。ビデオが完成した後、レベルがロードされ、エンジンがケースに入りましたが、Doom IIIにはまったく異なるストーリーがありました。


ステージ:


これを初めて見たとき、私はそれが何らかのトリックであると決めたことを覚えています。ビデオプレーヤーが中断され、デザイナーがディスプレイ画面にテクスチャを挿入し、カメラの位置がビデオの最後のフレームに対応すると考えました。私は間違っていました。idTech4は、ユーザーインターフェイスサーフェスのインタラクティブな要素でビデオを本当に再生できます。RoQはこれに使用されました。GrahamDevineがid Softwareに来たときに彼にもたらしたテクノロジーです。

興味深い事実:

イントロで使用されるRoQは2005年には印象的で、ゲーム内の画面で使用することは大胆な動きでした。


ただし、スクリプトとネイティブメソッドを呼び出す機能のおかげで、インタラクティブサーフェスはさらに多くの機能を備えています。

誰かが非常に興味を持っていて、彼らはどうにかしてDoom 1ローンチしました



興味深い事実:インタラクティブサーフェス技術は、すべてのDoom3メニュー(設定、メイン画面など)の作成にも使用されました。

より多くの興味深いもの...


このセクションは、レンダリングの氷山の一角にすぎず、さらに深く理解することができます

パート4:プロファイリング


Xcodeには、優れたプロファイリング方法であるInstrumentsがあります。ゲーム中、サンプリングモードで使用しました(ゲームの読み込みを完全に削除し、ビデオプロセッサでレベルを予備キャッシュします)。

復習




高レベルのループでは、プロセスで実行されている3つのスレッドがあります。


メインストリーム




メインのDoom 3スレッドは... QuakeMain驚いたことに、Mac OS XにQuake 3を移植したチームは古いコードを再利用したに違いありません。時間の再配布は内部で実行されます:


ゲームロジック




ゲームロジックは、スペースgamex86.dll(またはMac OS Xのgame.dylib)で実行されます。

ゲームロジックは、メインスレッド時間の25%を必要としますが、これは異常に大量です。これには2つの理由があります。


レンダラー




上記のように、レンダラーは2つの部分で構成されています。


負荷分散は十分にバランスが取れており、これは驚くことではありません。


レンダラー:フロントエンド




フロントエンドレンダラー:

驚くことではありませんが、ほとんどの時間(91%)がVBOからビデオプロセッサへのデータの読み込みに費やされています(R_AddModelSurfaces)。時間のごく一部(4%)が、地域間の移動とすべての相互作用の検索に費やされています(R_AddLightSurfaces)。最小時間(2.9%)は、目に見える表面の決定に費やされます:BSPをバイパスし、ポータルシステムを通過します。

レンダラー:バックエンド




レンダラーバックエンド:

明らかに、バックエンドはバッファーを変更し(GLimp_SwapBuffers)、画面との同期(10%)に時間を費やしています。これは、ゲームがダブルバッファー環境で実行されるためです。5%は、最初のパスで完全な再描画を回避するコストです。これは、主にZバッファー(RB_STS_FillDepthBuffer)を満たします。

裸の統計




Instrumentsトレースをダウンロードし、自分で探査を開始するように設定されている場合、プロファイルファイルは次のとおりです

パート4:スクリプト化された仮想マシン


idTech1からidTech3に毎回完全に変更された唯一の部分は、スクリプトシステムでした。


idTech4も例外ではなく、すべてが再び変更されました。


Doom3 Scripting SDKについての注意事項から始めるのが良いでしょう

建築


全体像は次のとおりです。

コンパイル:idCompiler 1つの定義済みファイルがブート時に転送されます.script。一連のディレクティブ#includeは、すべてのスクリプトの行とすべての関数のソースコードを含むスクリプトスタックを作成します。スキャンされidLexer、基本トークン(トークン)が生成されます。トークンが入りidParser、1つの巨大なバイトコードが生成されます。これはシングルトンに保存されidProgramます仮想マシンのRAMを表し、VM .textとのセグメントが含まれます.data



仮想マシン:実行時idThreadに、リンクリストの最後に達するまで、エンジンは各スレッドに実際のCPUの時間を(次々に)割り当てます。それぞれidThreadが含まれていますidInterpreter仮想マシンのCPUの状態を保存します。インタプリタが非常識になり、500万を超える命令に従わない限り、CPUはそれを空にすることはできません。それは共同マルチタスクです。

コンパイラ


コンパイルパイプラインは、プリプロセッサがないことを除けば、Google V8やClangなどの他のコンパイラーを読むときに表示されるものと似ています。したがって、「コメントスキップ」、マクロ、ディレクティブ(#include、#if)などの機能は、字句解析プログラムとパーサーの両方で実行する必要があります。

以来idLexer、広くすべてのテキストリソース(マップ、エンティティのルートカメラ)を解析するためにエンジン周りに使用、それは非常に原始的です。たとえば、合計5種類のトークンを返します。


したがって、パーサーは「標準」コンパイラーパイプラインよりも多くの作業を行います。

ゲームが開始されると、idCompilerは最初のスクリプトをロードします。script/doom_main.scriptシリーズ#includeは、1つの巨大なスクリプトに接続されたスクリプトのスタックを作成します。

エンジンパーサーは、再帰下降を備えた標準の下降パーサーのようです。スクリプト言語の文法はLL(1)であり、0のリターンが必要なようです(レキシカルアナライザーには単一のトークンの読み取りをキャンセルする機能があります)。すでにこの本を読んでいれば、迷うことはありません...しかし、そうでないなら、これは理解を始める良い理由です。

通訳


実行時に、イベントは作成をトリガーしますidThread。これは、オペレーティングシステムスレッドではなく、仮想マシンスレッドです。 CPUは一定の時間を与えます。それぞれidThreadidInterpreterコマンドポインターモニターと2つのスタック(1つはデータ/パラメーター用、もう1つは関数呼び出しの追跡用)があります。

実行はidInterpreter::Execute、インタープリターが仮想マシンの制御を停止するまで行われます。これは、共同マルチタスクです。

  idThread::Execute bool idInterpreter::Execute(void) { doneProcessing = false; while( !doneProcessing && !threadDying ) { instructionPointer++; st = &gameLocal.program.GetStatement( instructionPointer ); //op -      (unsigned short),     65 535  switch( st->op ) { . . . } } } 

idInterpreter制御を渡したidThread::Executeランタイムを必要とするスレッドがなくなるまで次のメソッドが呼び出されます。全体的なアーキテクチャは、Another World仮想マシンの図を強く思い出させました

興味深い事実:バイトコードは、アクティブな使用を目的としていないため、x86コマンドに変換されません。しかし、その結果、多くのことがスクリプトを介して行われるため、Doom3は、Quake3のように、JITからx86コマンドへの変換から大きな利益を得ます。

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


All Articles