私は喜んでQuake Worldのソースコードの研究に没頭し、私が理解したすべてを記事で概説しました。 これが整理したい人に役立つことを願っています。 この記事は4つのパートに分かれています。
- 建築
- ネットワーク
- 予測
- 可視化
建築
Quake Client
Quakeの学習は、
qwcl
(クライアント)プロジェクトから始める価値があります。
WinMain
エントリ
WinMain
はsys_win.cにあります。 要するに、コードは次のようになります。
WinMain { while (1) { newtime = Sys_DoubleTime (); time = newtime - oldtime; Host_Frame (time) { setjmp Sys_SendKeyEvents IN_Commands Cbuf_Execute /* */ CL_ReadPackets CL_SendCmd /* // */ CL_SetUpPlayerPrediction(false) CL_PredictMove CL_SetUpPlayerPrediction(true) CL_EmitEntities /* */ SCR_UpdateScreen } oldtime = newtime; } }
ここでは、Quake Worldの3つの主要な要素を強調できます。
- ネットワーク
CL_ReadPackets
およびCL_SendCmd
- 予測
CL_SetUpPlayerPrediction
、CL_PredictMove
およびCL_EmitEntities
- 視覚化
SCR_UpdateScreen
ネットワーク層(ネットチャンネルとも呼ばれます)は、
frames
情報(
frame_t
配列)にワールド情報を出力します。 これらは予測レイヤーに転送され、
cl_visedicts
衝突が処理され、データは可視性領域(POV)の定義とともに可視性表示(
cl_visedicts
)の形式で表示されます。 VisEdictは、POV変数(
cl.sim*
)とともに視覚化レイヤーで使用され、シーンをレンダリングします。
setjmp
:
コードに中間点を設定して、何か悪いことが起こった場合、プログラムはここに戻ります。
Sys_SendKeyEvents
:
Windows OSメッセージの受信、ウィンドウの最小化など。 エンジン変数の対応する更新(たとえば、ウィンドウが最小化されている場合、ワールドはレンダリングされません)。
IN_Commands
:
ジョイスティックのエントリに関する情報を取得します。
Cbuf_Execute
:
ゲームの各サイクルで、コマンドはバッファーで実行されます。 コマンドは主にコンソールを介して生成されますが、サーバーまたはキーストロークからも生成できます。
ゲームは、コマンドバッファーの
exec quake.rc
から開始します。
CL_ReadPackets
および
CL_SendCmd
:
エンジンのネットワーク部分の処理。
CL_SendCmd
は、マウスとキーボードの入力をインターセプトし、コマンドを生成して送信します。
Quake WorldはUDPを使用していたため、netChannelパケットヘッダーに設定されたシーケンス/ sequenceACKによって伝送の信頼性が保証されました。 さらに、最後のコマンドは体系的に再送されました。 クライアント側では、パッケージの転送に制限はなく、更新は可能な限り頻繁に送信されました。 サーバー側では、パケットが受信され、送信速度が処理速度よりも遅い場合にのみ、メッセージがクライアントに送信されました。 この制限はクライアントによって設定され、サーバーに送信されました。
「ネットワーク」セクション全体がこのトピック専用です。
CL_SetUpPlayerPrediction
、
CL_PredictMove
および
CL_EmitEntities
:
エンジンでの予測と衝突の計算を実行しました。 これらは主に、ネットワーク上の伝送遅延に対処するように設計されています。
このトピックは、「予測」セクション全体で説明されています。
SCR_UpdateScreen
:
エンジンでの視覚化 。 この部分では、BSP / PVSが積極的に使用されます。 ここで
include
/
define
基づいてコードが分岐し
define
。 Quakeエンジンは、プログラムで、またはハードウェアアクセラレーションで世界をレンダリングできます。
「視覚化」セクションはこれに完全に専念しています。
zipアーカイブを開いてコンパイルする
zipを開く:
q1sources.zipアーカイブには、
QW
と
WinQuake
という2つのフォルダー/ 2つのVisual Studioプロジェクトがあり
WinQuake
。
-
WinQuake
は、クライアントとサーバーが組み合わされたコードであり、単一のプロセスとして動作します(DOSでサポートされている場合、これらは2つの別個のプロセスであることが理想的です)。 ネットワークプレイはLAN上でのみ可能でした。 -
QW
は、サーバーとクライアントを別々のマシンで実行する必要のあるQuake Worldプロジェクトです(クライアントWinMain
はWinMain
(sys_win.c
)であり、サーバーエントリポイントはmain
(sys_win.c
)であることにsys_win.c
してsys_win.c
) 。
OpenGLレンダリングでQuake Worldを学びました。 このプロジェクトには4つのサブプロジェクトがあります。
-
gas2asm
アセンブラコードをGNU ASMからx86 ASMに移植するためのユーティリティ -
qwcl
-Quakeクライアント側 -
QWFwd
-Quakeサーバーの前にあるプロキシ -
qwsv
-Quakeqwsv
コンパイル:
WindowsとDirectX SDKをインストールした後、Visual Studio 2008でのコンパイルで1つのエラーが検出されます。
.\net_wins.c(178) : error C2072: '_errno' : initialization of a function
_errno
は現在、他の何かに使用されるMicrosoftマクロです。 これらのエラーを修正するには、変数名を
_errno
置き換えます(例:
qerrno
。
net_wins.c
if (ret == -1) { int qerrno = WSAGetLastError(); if (qerrno == WSAEWOULDBLOCK) return false; if (qerrno == WSAEMSGSIZE) { Con_Printf ("Warning: Oversize packet from %s\n", NET_AdrToString (net_from)); return false; } Sys_Error ("NET_GetPacket: %s", strerror(qerrno)); }
リンカは、qwclプロジェクトのLIBC.libについて文句を言います。 無視されたライブラリ「Ignored Library」のリストに追加するだけで、4つのプロジェクトのアセンブリが完了します。
ツール
Visual Studio Express(フリーウェア)はIDEとして最適でした。 BSP / PVS、Id Software、およびQuakeに基づいてエンジンをよりよく理解したい場合は、いくつかの本を読むことをお勧めします。
Quake Source Code週間の本棚は次のようになりました。
ネットワーク
QuakeWorldのネットワークアーキテクチャは、かつて素晴らしい革新と見なされていました。 後続のすべてのネットワークゲームは同じアプローチを使用しました。
ネットワークスタック
Quakeの情報交換の基本単位は
でした。 それらは、位置、方向、健康、プレーヤーへのダメージなどを更新するために使用されます。 TCP / IPには、リアルタイムシミュレーション(送信制御、信頼性の高い配信、パケット順序の保存)に役立つ多くの優れた機能がありますが、このプロトコルはQuake Worldエンジンでは使用できません(元のQuakeで使用されていました)。 一人称シューティングゲームでは、時間通りに受信されなかった情報は再送する価値がありません。 したがって、UDP / IPが選択されました。 信頼性の高い配信を保証し、パケットの順序を維持するために、抽象化のネットワーク層「
NetChannel
」を作成しました。
OSIの観点から見ると、
NetChannel
UDPの上に便利に配置されています。
要約すると、エンジンは主に
機能し
。 データを送受信する必要がある場合、彼はこのタスクを
Netchan_Transmit
および
Netchan_Process
に
netchan.c
(これらのメソッドはクライアントとサーバーで同じです)。
NetChannelヘッダー
NetChannelヘッダーの構造は次のとおりです。
ビットオフセット | ビット0-15 | 16-31 |
---|---|---|
0 | シーケンス | |
32 | ACKシーケンス | |
64 | Qport | チーム |
94 | ... |
- シーケンスは、送信者によって初期化され、パケットが送信されるたびに1ずつ増加する
int
番号です。Sequence
多くの目的で使用されますが、最も重要なタスクは、受信者に失われた/重複した/異常なUDPパケットを認識する機能を提供することです。 この整数の最上位ビットはシーケンスの一部ではなく、(
)に信頼できるデータが含まれているかどうかを示すフラグです(これについては後で説明します)。 - ACKシーケンスも
int
で、最後に受信したシーケンス番号と同じです。 彼のおかげで、NetChannelの反対側はパケットが失われたことを理解できます。 - QPortはNATルーターの回避策です(詳細については、このセクションの最後を参照してください)。 その値は、クライアントの起動時に設定される乱数です。
- コマンド:重要なデータが送信されました。
信頼できるメッセージ
信頼できないコマンドはUDPパケットにグループ化され、最後の発信シーケンス番号でマークされて送信されます。送信者にとって、失われるかどうかは関係ありません。 信頼できるコマンドの処理方法は異なります。 主なことは、送信者と受信者の間に未確認の信頼できるUDPパケットが1つしか存在できないことを理解することです。
各ゲームサイクルでは、新しい信頼できるチームを生成するときに、
message_buf
配列に追加されます(
message
変数で制御されます)( 1 )。 信頼できるコマンドのセットは、
message
から
reliable_buf
( 2 )配列に移動され
message
。 これは、
reliable_buf
空の場合にのみ発生します(空でない場合、別のコマンドセットが以前に送信され、受信がまだ確認されていないことを意味します)。
次に、最終的なUDPパケットが形成されます。NetChannel( 3 )ヘッダーが追加され、次にtrusted_bufの内容と現在の
reliable_buf
コマンド(十分なスペースがある場合)が追加されます。
受信側では、UDPメッセージが解析され、着信
sequence
番号が発信
sequence ACK
( 4 )に送信され
sequence ACK
(パケットに信頼できるデータが含まれていることを示すビットフラグと共に)。
次に受け取るメッセージ:
- 信頼性ビットフラグがtrueの場合、UDPパケットは受信者に配信されています。 NetChannelは、
reliable_buf
( 5 )をクリアでき、新しいコマンドセットを送信する準備ができています。 - 信頼性ビットフラグがfalseの場合、UDPパケットは受信者に到達していません。 NetChannelは、
reliable_buf
の内容の送信を再試行します。 新しいコマンドはmessage_buf
蓄積さmessage_buf
ます。 配列がオーバーフローすると、クライアントはリセットされます。
トランスミッション制御
私が理解しているように、送信制御はサーバー側でのみ実行されます。 クライアントは、可能な限り頻繁にステータスの更新を送信します。
サーバー上でのみアクティブな伝送制御の最初のルール:パケットがクライアントから受信された場合にのみパケットを送信します。 2番目のタイプの伝送制御は「チョーク」です。これは、クライアントがコンソールコマンド
rate
設定するパラメーターです。 サーバーは更新メッセージをスキップして、クライアントに送信されるデータの量を削減できます。
重要なコマンド
コマンドには、
に格納されたタイプコードと、それに続く有用なコマンド情報が含まれます。 おそらく最も重要なのは、ゲームの状態に関する情報を提供するコマンド(
frame_t
)です。
-
svc_packetentities
およびsvc_deltapacketentities
:ミサイルトレース、爆発、パーティクルなどのオブジェクトを更新します。 -
svc_playerinfo
:プレーヤーの位置、最後のチーム、およびチームの継続時間に関する更新をミリ秒単位でsvc_playerinfo
します。
qportの詳細を読む
エラーを修正するために、QportがNetChannelヘッダーに追加されました。 qportの前に、Quakeサーバーは「リモートIPアドレス、リモートUDPポート」の組み合わせを使用してクライアントを識別しました。 ほとんどの場合、これはうまく機能しましたが、一部のNATルーターはポート変換スキーム(リモートUDPポート)を任意に変更できます。 UDPポートは信頼できなくなり、John Carmackは、「リモートIPアドレス、NetChannelヘッダーのQport」によってクライアントを識別することにしたと説明しました。 これによりエラーが修正され、サーバーは宛先UDP応答ポートを即座に変更できました。
遅延計算
Quakeエンジンは、
frame_t
とともに、最近送信された64個のコマンド(
frame_t
:
frames
配列)を
senttime
ます。 それらは、送信に使用されたシーケンス番号(
outgoing_sequence
)によって直接アクセスできます。
frame = &cl.frames[cls.netchan.outgoing_sequence & UPDATE_MASK]; frame->senttime = realtime; //
サーバーから確認を受け取った後、コマンドを送信する時間は
sequenceACK
から取得され
sequenceACK
。 遅延は次のように計算されます。
// frame = &cl.frames[cls.netchan.incoming_acknowledged & UPDATE_MASK]; frame->receivedtime = realtime; latency = frame->receivedtime - frame->senttime;
エレガントなソリューション
配列インデックスのループ
エンジンのネットワーク部分には、最後に受信した64個のUDPパケットが格納されます。 配列をループする単純な解決策は、整数除算の剰余演算子を使用することです。
arrayIndex = (oldArrayIndex+1) % 64;
代わりに、UPDATE_MASKのバイナリAND演算を使用して新しい値が計算されます。 UPDATE_MASKは64-1です。
arrayIndex = (oldArrayIndex+1) & UPDATE_MASK;
実際のコードは次のようになります。
frame_t *newpacket; newpacket = &frames[cls.netchan.incoming_sequence&UPDATE_MASK];
更新:これは、剰余除算操作の最適化に関してディートリッヒEppから受け取ったコメントです。
, "". : file.c: unsigned int modulo(unsigned int x) { return x % 64; } unsigned int and(unsigned int x) { return x & 63; } gcc -S file.c file.s. , , ! "" << 5 *32. , , , << 5 & 63 "", *32 %64 . --Dietrich .globl modulo .type modulo, @function modulo: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax andl $63, %eax popl %ebp ret .size modulo, .-modulo .globl and .type and, @function and: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax andl $63, %eax popl %ebp ret .size and, .-and
予測
ネットワーク通信のためのNetChannel抽象化を見ました。 次に、遅延が予測によってどのように相殺されるかを調べます。 学習する資料は次のとおりです。
予測
予測はおそらく、Quake Worldエンジンの中で最も難しく、文書化されておらず、最も重要な部分です。 予測の目的は、待ち時間を無効にすること、つまり、情報を送信するために媒体が必要とする遅延を補償することです。 予測はクライアント側で実行されます。 このプロセスは「クライアント側予測」と呼ばれます。 サーバー側では、遅延補正手法は適用されません。
問題:
ご覧のとおり、ゲームの状態はレイテンシの半分の値だけ「古い」状態です。 チームを送信する時間を追加する場合、アクションの結果を確認するためにフルサイクル(待機時間)を待つ必要があります。
Quakeの予測システムを理解するには、NetChannelが
frames
変数(array
frame_t
)をどのように取り込むかを理解する必要があります。
サーバーに送信される各コマンドは、
netchannel.outgoingsequence
インデックスで
senttime
とともに
frames
に保存されます。
サーバーが
sequenceACK
を使用してコマンドの受信を確認すると、送信されたコマンドを受け入れて待機時間を計算できます。
latency = senttime-receivedtime;
この段階では、 遅延/ 2秒前の世界がわかります。 NATでは、待ち時間は非常に短い(<50ミリ秒)が、インターネットでは非常に大きく(> 200ミリ秒)、 現在の世界の状態をシミュレートするには予測が必要です。 このプロセスは、ローカルプレーヤーと他のプレーヤーで異なる方法で実行されます。
ローカルプレイヤー
ローカルプレーヤーの場合、サーバーの状態を推定するため、レイテンシはほぼ0に減少します。 これは、サーバーから受信した最後のステータスを使用して、その瞬間から送信されたすべてのコマンドを「再生」します。
したがって、クライアントは、サーバー上のその位置が時間t +遅延/ 2になると予測します。
コードの観点から、これは
CL_PredictMove
メソッドを使用して行われます。 まず、Quakeエンジンは「再生可能な」コマンドのセンチメント制限を選択します。
cl.time = realtime - cls.latency - cl_pushlatency.value*0.001;
注:
cl_pushlatency
は、値がクライアント側で設定されるコンソール変数(cvar)です。 これは、ミリ秒単位のクライアントの負の遅延に相当します。 これから、
cl.time = realtime
と結論付けるのは簡単です。
その後、他のすべてのプレーヤーは
CL_SetSolidPlayers (cl.playernum);
定義され
CL_SetSolidPlayers (cl.playernum);
(衝突をテストできるようにするため)ソリッドオブジェクトとして、最後に受信した状態からその瞬間に送信された「再生」コマンド:
cl.time <= to->senttime
(衝突は
CL_PredictUsercmd
を使用して各反復でテストされます)。
他のプレイヤー
他のプレイヤーの場合、Quakeエンジンには「送信済みですが、まだ確認されていないチーム」がないため、代わりに補間が使用されます。 最後の既知の位置から開始して、
cmd
結果の位置を予測するために補間されます。 角回転なしで位置のみが予測されます。
Quake Worldでは、他のプレイヤーのレイテンシも考慮されます。 各プレイヤーのレイテンシーは、ワールドの更新とともに送信されます。
コード
衝突予測および計算コードは、次のように要約できます。
CL_SetUpPlayerPrediction(false) CL_PredictMove | /* */ | CL_SetSolidPlayers | | CL_PredictUsercmd | | PlayerMove | CL_SetUpPlayerPrediction(true) CL_EmitEntities CL_LinkPlayers | /* */ | | | CL_SetSolidPlayers | | CL_PredictUsercmd | | PlayerMove CL_LinkPacketEntities CL_LinkProjectiles CL_UpdateTEnts
Quake Worldはプレイヤーの予測を実行するだけでなく、予測に基づいて衝突を認識するため、この部分は複雑です。
CL_SetUpPlayerPrediction(false)
最初の呼び出しは予測を実行せず、サーバーから受信した位置にプレイヤーを配置するだけです(つまり、tレイテンシ/ 2の遅延で)。
CL_PredictMove()
これは、ローカルプレーヤーが移動する場所です。
- 方向は補間されず、リアルタイムで完全に実行されます。
- 位置と速度:現在までに送信されたすべてのコマンド(
cl.time <= to->senttime
)は、サーバーから受信した最後の位置/速度に適用されます。
位置と速度の更新の詳細:
- 最初に、他のプレイヤーは
CL_SetUpPlayerPrediction(false)
を使用して、CL_SetUpPlayerPrediction(false)
設定された最後の既知の位置で)ソリッドオブジェクトにCL_SetSolidPlayers
ます。 - エンジンは、送信されたすべてのコマンドを繰り返し、衝突をチェックし、
CL_PredictUsercmd
を使用して位置を予測しCL_PredictUsercmd
。 衝突は、他のプレーヤーでもテストされます。 - 結果の位置と速度は
cl.sim*
保存されcl.sim*
。 これらは後で視点を調整するために使用されます。
CL_SetUpPlayerPrediction(true)
2番目のサーバー側の呼び出しでは、現在の時点での他のプレーヤーの位置が予測されます(ただし、移動はまだ実行されていません)。 位置は、最後の既知のチームと最後の既知の位置に基づいて推定されます。
注:ここで小さな問題が発生します。Valveは(
cl_pushlatency
)サーバー側のローカルプレーヤーのステータスをt +レイテンシ/ 2で予測することを推奨します。 ただし、他のプレーヤーの位置は、時間tでサーバー側で予測されます。 おそらく、QWの
cl_pushlatency
の最適な値は-latency / 2でしたか?
CL_EmitEntities
可視性ガイドラインはここで生成されます。 次に、レンダラーに渡されます。
- CL_LinkPlayers:他のプレイヤーが動いており、他のプレイヤーがソリッドオブジェクトに変わり、予測された位置に対して衝突検出が実行されます。
- CL_LinkPacketEntitiesPacket:サーバーから受信した最後の状態のオブジェクトが予測され、可視性ガイドラインに関連付けられます。 だから、打ち上げられたロケットには遅れがあります。
- CL_LinkProjectiles:釘およびその他のシェルの処理。
- CL_UpdateTEnts:光線とオブジェクトの標準更新。
可視化
オリジナルのゲームを開発するとき、ほとんどの努力はQuakeレンダラーモジュールに費やされました。 これについては、Michael Abrashの本とJohn Carmackの.planファイルに詳しく説明されています。
可視化
シーンの視覚化プロセスは、BSPカードと密接にリンクしています。 ウィキペディアのバイナリスペースパーティション分割について詳しく読むことをお勧めします。 要するに、Quakeカードは多くの前処理を経ました。 ボリュームは次のように再帰的にカットされました。
このプロセスにより、リーフを含むBSPが作成されました(作成ルールは次のとおりです。既存のポリゴンを切断面として選択し、より少ないポリゴンを切断するセパレータを選択します)。 BSPを作成した後、PVS(潜在的に可視のセット、潜在的に可視のセット)が各シートに対して計算されました。 例:シート4は、リーフ7および9を潜在的に見ることができます。
このシートの最終PVSはビットベクトルとして保存されました。
Id 葉っぱ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
シート4のPVS | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
シート4の圧縮PVS | 3 | 2 | 1 | 7 |
---|
エンコードされたPVSには、ユニット間のゼロの数のみが含まれていました。 これは非常に効果的な圧縮技術のようには見えませんが、多数の葉(32767)と非常に限られた可視の葉のセットを組み合わせることで、PVS全体のサイズが20KBに縮小されました。
アクションの前処理
事前に計算されたBPSおよびPVSが存在するため、エンジンでマップを視覚化するプロセスは簡単でした。
- BSPをバイパスして、カメラが指しているシートを判別します。
- このシートのPVSを抽出して展開し、PVSを反復処理して、BSPでリーフにマークを付けます。
- 近くから遠くまでBSPをバイパスします。
- ノード(ノード)がマークされていない場合、スキップされます。
- カメラの可視性のピラミッド内の存在について、ノードの境界全体をテストします。
- 現在のシートを視覚化リストに追加します。
注: BSPは複数回使用されます。たとえば、マップを最も近いポイントから各アクティブな光源の距離にバイパスし、マップ上のポリゴンにマークを付ける場合。
注2:ソフトウェアレンダリングでは、BSPツリーは遠いポイントから最も近いポイントまでトラバースされました。
コード分析
つまり、視覚化コードは次のように表すことができます。
SCR_UpdateScreen { GL_BeginRendering SCR_SetUpToDrawConsole V_RenderView | R_Clear | R_RenderScene | | R_SetupFrame | | Mod_PointInLeaf | | R_SetFrustum | | R_SetupGL | | R_MarkLeaves | | | Mod_LeafPVS | | | Mod_DecompressVis | | R_DrawWorld | | | R_RecursiveWorldNode | | | DrawTextureChains | | | | R_RenderBrushPoly | | | | DrawGLPoly | | | R_BlendLightmaps | | S_ExtraUpdate | | R_DrawEntitiesOnList | | GL_DisableMultitexture | | R_RenderDlights | | R_DrawParticles | R_DrawViewModel | R_DrawAliasModel | R_DrawWaterSurfaces | R_PolyBlend GL_Set2D SCR_TileClear V_UpdatePalette GL_EndRendering }
SCR_UpdateScreen
呼び出し:
-
GL_BeginRendering
(glx,gly,glwidth,glheight
後でR_SetupGL
ビューポートと投影行列を設定するために使用される変数()の値を設定します) -
SCR_SetUpToDrawConsole
(コンソールの高さを決定します:2Dに関連する部分ではなく、なぜここにあるのですか?!) -
V_RenderView
(3Dシーンレンダリング) -
GL_Set2D
(正射影への切り替え(2D)) -
SCR_TileClear
(多くの2Dオブジェクト、コンソール、FPSメトリックなどの追加レンダリング) -
V_UpdatePalette
(名前はソフトウェアレンダラーに対応します。openGLでは、メソッドは受け取ったダメージまたはアクティブなボーナスに応じてミキシングモードを設定し、画面を赤くしたり、明るくしたりします。)値はに格納されますv_blend
-
GL_EndRendering
(バッファスイッチング(ダブルバッファリング)!)
V_RenderView
呼び出し:
-
V_CalcRefdef
(すみません、私はこの部分を理解していませんでした) -
R_PushDlights
各光源でポリゴンをマークして、効果を適用します(注を参照) -
R_RenderView
注: R_PushDlightsは、再帰メソッド(
R_MarkLights
)を呼び出します。BSPを使用して、光源の影響を受けるポリゴンを(整数ビットベクトルを使用して)マークします。BSPは(光源の観点から)近い点から遠い点に移動します。このメソッドは、光源がアクティブで手の届く範囲にあるかどうかを確認します。この方法は
R_MarkLights
特に注目に値します。なぜなら、ここではポイントとプレーン間の距離に関するMichael Abrashによる記事の直接的な実装が見られるからです(「参照フレーム」
dist = DotProduct (light->origin, splitplane->normal) - splitplane->dist;
)())。
R_RenderViewの
呼び出し:
-
R_Clear
(必要であればGL_COLOR_BUFFER_BITおよび/またはGL_DEPTH_BUFFER_BITのクリーニング) -
R_RenderScene
-
R_DrawViewModel
(オブザーバーモードでのプレーヤーモデルのレンダリング) -
R_DrawWaterSurfaces
(GL_BEND / GL_MODULATEモードに切り替えて水を引く。変形は、ルックアップテーブルsinおよびcosを使用して実行されますgl_warp.c
) -
R_PolyBlend
(V_UpdatePalette
変数に設定された値を使用して画面全体を混合しますv_blend
。これは、ダメージを受ける(赤)、水中にいる、またはボーナスを適用することを示すために使用されます)
R_RenderScene
呼び出し:
-
R_SetupFrame
(カメラが配置されているBSPシートを抽出し、変数「r_viewleaf」に保存します) -
R_SetFrustum
(インストールmplane_t[4]
。近くと遠い飛行機なし。 -
R_SetupGL
(GL_PROJECTION、GL_MODELVIEW、glCullFaceのビューポートと側面の設定、およびY軸とZ軸の回転。QuakeのX軸とZ軸はopenGLとは異なる位置にあるため。) -
R_MarkLeaves
-
R_DrawWorld
-
S_ExtraUpdate
(マウスの位置をリセットし、オーディオの問題を解決します) -
R_DrawEntitiesOnList
(リスト内のオブジェクトの描画) -
GL_DisableMultitexture
(マルチテクスチャリングを無効にする) -
R_RenderDlights
(ライトドメインと照明効果) -
R_DrawParticles
(爆発、火、電気など)
R_SetupFrame
興味深い行:
r_viewleaf = Mod_PointInLeaf (r_origin, cl.worldmodel);
その中で、Quakeエンジンは、カメラが現在指しているBSPのシート/ノードを取得します。
Mod_PointInLeafはmodel.cにあり、BSPを介して実行されます(BSPツリーのルートはmodel->ノードにあります)。
各ノードに対して:
- ノードがさらにスペースを分析しない場合、それはシートなので、現在のノードの位置として戻ります。
- それ以外の場合、BSPの正割面は現在の位置についてチェックされ(通常のスカラー積を使用して、これはBSPツリーをトラバースする標準的な方法です)、対応する子はバイパスされます。
R_MarkLeaves BSPのカメラの場所を
変数
r_viewleaf
(に取得
R_SetupFrame
)に格納し、潜在的な可視セット(PVS)を検索(
Mod_LeafPVS
)、および解凍(
Mod_DecompressVis
)します。次に、ビットベクトルを反復処理し、潜在的に表示されるBSPノードをマークします:node-> visframe = r_visframecount。
R_DrawWorldの
課題:
-
R_RecursiveWorldNode
(BSPワールドを前から後ろに走査し、以前にマークされていないノードをスキップし(cR_MarkLeaves
)、cl.worldmodel->textures[]->texturechain
適切なポリゴンでリストに入力します。) -
DrawTextureChains
(texturechainに保存されているポリゴンのリストを描画する:cl.worldmodel-> textures []を反復処理します。これにより、マテリアルへのスイッチを1つだけ取得できます。悪くはありません。) -
R_BlendLightmaps
(フレームバッファーでライトマップを混合するために使用される2番目のパス)。
注:
この部分では、悪名高いopenGLの「イミディエイトモード」モードが使用されます。このモードは「技術の最後の言葉」と見なされていました。
で
R_RecursiveWorldNode
クリッピング表面のほとんどの操作が実行されます。次の場合、ノードは切断されます。
- その内容は固体オブジェクトです。
- シートはPVSでマークされていません(
node->visframe != r_visframecount
) - 葉は可視性のピラミッドに沿ってクリッピングされません。
MDL形式
MDL形式は、固定フレームのセットです。Quakeエンジンは、頂点の位置を補間してアニメーションを滑らかにしません(したがって、フレームレートが高くてもアニメーションは改善されません)。
エレガントなソリューション
エレガントなリーフ
タギングレンダリングのためのBSP リーフタギングの素朴なアプローチは、ブール変数を使用することです
isMarkedVisible
。各フレームの前に必要なもの:
- すべてのブール変数の値をfalseに設定します。
- PVSを繰り返しバイパスし、表示されている各シートにtrueを指定します。
- 次に、シートをテストします
if (leave.isMarkedVisible)
代わりに、Quakeエンジンは整数を使用して、レンダリングされたフレーム(
r_visframecount
変数)の数を計算します。これにより、最初のステップを取り除くことができます。
- 反復PVSトラバーサルおよび可視シートセットごと
leaf.visframe = r_visframecount
- 次に、シートをテストします
if (leaf.visframe == r_visframecount)
再帰を取り除く
には
R_SetupFrame
代わりにBSPのための「間に合わせ」トラバーサル再帰を実行しながらサイクルを利用して現在位置を取得します。
node = model->nodes; while (1) { if (node->contents < 0) return (mleaf_t *)node; plane = node->plane; d = DotProduct (p,plane->normal) - plane->dist; if (d > 0) node = node->children[0]; else node = node->children[1]; }
テクスチャの切り替えの最小化
openGLでは、(
glBindTexture(GL_TEXTURE_2D,id)
)を使用したテクスチャの切り替えは非常に高価です。テクスチャの切り替え回数を最小限に抑えるため、レンダリング用にマークされた各ポリゴンは、ポリゴンテクスチャマテリアルによってインデックス付けされた配列のチェーンに格納されます。
cl.worldmodel->textures[textureId]->texturechain[]
クリッピングが完了すると、テクスチャチェーンが順番に描画されます。したがって、合計N個のテクスチャスイッチが実行されます。ここで、Nは可視テクスチャの総数です。
int i; for ( i = 0; i < cl.worldmodel->textures_num ; i ++) DrawTextureChains(i);