複数のGLSLシェーダー間でコードを共有する


30

多くの場合、いくつかのシェーダー間でコードをコピー&ペーストします。これには、単一のパイプライン内のすべてのシェーダー間で共有される特定の計算またはデータ、およびすべての頂点シェーダー(またはその他のステージ)が必要とする共通の計算の両方が含まれます。

もちろん、それは恐ろしい習慣です。コードをどこかで変更する必要がある場合は、他のすべての場所でコードを変更する必要があります。

DRYを維持するための承認済みのベストプラクティスはありますか?人々はすべてのシェーダーに単一の共通ファイルを追加するだけですか?彼らは#includeディレクティブを解析する独自の初歩的なCスタイルのプリプロセッサを書いていますか?業界で受け入れられているパターンがある場合は、それらをフォローしたいと思います。


4
他のいくつかのSEサイトはベストプラクティスに関する質問を望んでいないため、この質問は少し物議をかもします。これは、このコミュニティがこのような質問に関してどのように位置付けられているかを確認するためのものです。
マーティンエンダー

2
うーん、よさそうだ。私たちの質問では、StackOverflowよりもかなり「広範」/「より一般的」だと思います。
クリスは、モニカを復活させる

2
StackOverflowは、「お問い合わせください」から「お願いがありますまでお問い合わせください」ボードになりました。
インサイド

話題性を判断することを意図している場合、関連するメタ質問はどうですか?
SLバース-モニカーの復活

回答:


18

たくさんのアプローチがありますが、完璧なものはありません。

glAttachShaderシェーダーを組み合わせて使用することでコードを共有することは可能ですが、これにより構造体宣言や#define-d定数などを共有することはできません。機能を共有するために機能します。

渡される文字列の配列をglShaderSourceコードの前に共通の定義を追加する方法として使用したい人もいますが、これにはいくつかの欠点があります。

  1. シェーダー内から何を含める必要があるかを制御するのは困難です(これには別のシステムが必要です)。
  2. これは、#versionGLSL仕様の次のステートメントにより、シェーダー作成者がGLSLを指定できないことを意味します。

#versionのディレクティブは、コメントと空白を除いて、他の何かの前にシェーダで発生する必要があります。

この文のためglShaderSource#version宣言の前にテキストを追加するために使用することはできません。これは、その#version行をglShaderSource引数に含める必要があることを意味します。つまり、GLSLコンパイラインターフェイスは、使用するGLSLのバージョンを何らかの方法で通知する必要があることを意味します。さらに、a #versionを指定しないと、GLSLコンパイラはデフォルトでGLSLバージョン1.10を使用します。シェーダー作成者#versionが標準的な方法でスクリプト内を指定できるようにする場合#includeは、#versionステートメントの後に何らかの方法で-sを挿入する必要があります。これは、GLSLシェーダーを明示的に解析して#version文字列(存在する場合)を検索し、その後にインクルードすることで実行できますが、#includeこれらのインクルードを作成する必要がある場合は、より簡単に制御するためにディレクティブを使用することをお勧めします。一方、GLSL #versionは行の前のコメントを無視するため、ファイルの上部にあるコメント内のインクルードにメタデータを追加できます(うん)。

質問は次のとおりです:の標準的な解決策はあり#includeますか、それとも独自のプリプロセッサ拡張を展開する必要がありますか?

GL_ARB_shading_language_include拡張機能はありますが、いくつかの欠点があります。

  1. NVIDIAのみでサポートされています(http://delphigl.de/glcapsviewer/listreports2.php?listreportsbyextension=GL_ARB_shading_language_include
  2. 事前にインクルード文字列を指定することで機能します。したがって、コンパイルする前に、文字列"/buffers.glsl"(で使用される#include "/buffers.glsl")がファイルの内容buffer.glsl(以前にロードしたもの)に対応することを指定する必要があります。
  3. ポイント(2)でお気づきかもしれませんが"/"、Linuxスタイルの絶対パスのように、パスはで始まる必要があります。この表記法は一般にCプログラマには馴染みがないため、相対パスを指定できないことを意味します。

一般的な設計では独自の#includeメカニズムを実装しますが、#if条件付きコンパイル(ヘッダーガードなど)を適切に処理するために、他のプリプロセッサ命令も解析(および評価)する必要があるため、これは難しい場合があります。

独自のを実装する場合、それを実装する#include方法にもいくつかの自由があります。

  • 事前に文字列を渡すことができます(などGL_ARB_shading_language_include)。
  • インクルードコールバックを指定できます(これはDirectXのD3DCompilerライブラリによって行われます)。
  • 通常のCアプリケーションで行われるように、常にファイルシステムから直接読み取るシステムを実装できます。

簡略化として、プリプロセスレイヤーの各インクルードにヘッダーガードを自動的に挿入できるため、プロセッサーレイヤーは次のようになります。

if (#include and not_included_yet) include_file();

(上記のテクニックを見せてくれたトレント・リードへのクレジット。)

結論として、自動、標準、および単純なソリューションは存在しません。将来のソリューションでは、いくつかのSPIR-V OpenGLインターフェイスを使用できます。その場合、GLSLからSPIR-VへのコンパイラはGL APIの外部にある可能性があります。コンパイラをOpenGLランタイムの外部に#include配置すると、ファイルシステムとのインターフェイスに適した場所になるため、実装が大幅に簡素化されます。現在広く普及している方法は、Cプログラマーなら馴染みのある方法で動作するカスタムプリプロセッサを実装することだと思います。


シェーダーはglslifyを使用してモジュールに分割することもできますが、node.jsでのみ機能します。
アンダーソングリーン

9

私は通常、glShaderSource(...)が入力として文字列の配列を受け入れるという事実を使用します。

jsonベースのシェーダー定義ファイルを使用します。これは、シェーダー(またはより正確に言えばプログラム)の構成方法を指定し、そこで必要なプリプロセッサ定義、使用するユニフォーム、頂点/フラグメントシェーダーファイル、すべての追加の「依存関係」ファイル。これらは、実際のシェーダーソースの前にソースに追加される関数のコレクションです。

AFAIKを追加するだけで、Unreal Engine 4は、#includeディレクティブを使用します。これは、提案されたように、コンパイルの前にすべての関連ファイルを解析して追加します。


4

共通の慣習はないと思いますが、推測すると、ほとんどすべての人が前処理ステップ(#include拡張機能)としてテキスト形式のインクルードの簡単な形式を実装していると思います。そう。(JavaScript / WebGLでは、たとえば単純な正規表現を使用してこれを行うことができます)。この利点は、シェーダーコードを変更する必要がなくなったときに、「リリース」ビルドのオフラインステップで前処理を実行できることです。

実際、このアプローチが一般的であることを示すのは、ARB拡張がそのために導入されたという事実ですGL_ARB_shading_language_include。これが今ではコア機能になったかどうかはわかりませんが、拡張機能はOpenGL 3.2に対して書かれています。


2
GL_ARB_shading_language_includeはコア機能ではありません。実際、NVIDIAのみがサポートしています。(delphigl.de/glcapsviewer/...
ニコラ・ルイのGuillemot

4

glShaderSource文字列の配列をとることができることを既に指摘している人もいます。

その上、GLSL では、シェーダーのコンパイル(glShaderSourceglCompileShader)とリンク(glAttachShaderglLinkProgram)は分離されています。

いくつかのプロジェクトでそれを使用して、特定の部分とほとんどのシェーダーに共通の部分の間でシェーダーを分割し、すべてのシェーダープログラムでコンパイルおよび共有します。これは機能し、実装するのは難しくありません。依存関係リストを維持するだけです。

しかし、保守性に関しては、それが勝利であるかどうかはわかりません。観察結果は同じでした。因数分解してみましょう。それは確かに繰り返しを避けますが、技術のオーバーヘッドは重要に感じます。さらに、最終シェーダーの抽出はより困難です。一部のコンパイラーが拒否または複製する順序で宣言が終了するため、シェーダーソースを単に連結することはできません。そのため、別のツールで簡単なシェーダーテストを行うのが難しくなります。

最終的に、この手法はいくつかのDRYの問題に対処しますが、理想とはほど遠いものです。

サイドトピックでは、このアプローチがコンパイル時間の点で影響があるかどうかはわかりません。一部のドライバーはリンク時にシェーダープログラムを実際にコンパイルするだけですが、測定していません。


私の理解から、これは構造体定義を共有する問題を解決しないと思います。
ニコラス・ルイ・ギレモット

@NicolasLouisGuillemot:はい、そうです。この方法で共有されるのは命令コードだけであり、宣言ではありません。
ジュリアンゲルトー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.