• 検索結果がありません。

_openglcl

N/A
N/A
Protected

Academic year: 2021

シェア "_openglcl"

Copied!
164
0
0

読み込み中.... (全文を見る)

全文

(1)

この講義では、データを高速に処理する手法を解説する。特に最近、急速に性能が高まっ

2

ているGPU(Graphics Processing Unit1)を用いた高速計算について二つの視点から解

3 説を行う。 4 ひとつはコンピュータグラフィックス(CG)である。言うまでもなく、GPUはCGの高 5 速描画のためのデバイスとして開発されてきた。CGの描画技法のひとつであるラスタラ 6 イズ法は定型的な処理であるため、ハードウェアで高速化することに向いており、実際に、 7 GPUはラスタライズ法をハードウェアで実装している。この講義では、OpenGL/GLSL 8 を用いてその概要を学ぶ。 9 知能情報システム学科2年生科目「コンピュータグラフィックス」でもCGを解説した。 10 しかし、そこで述べた内容は2D CGや3D CGの基礎であり、GPUを用いたCG描画に 11 ついてはほとんど述べていない。逆にこの講義は、2D CGや3D CGの様々なテクニック 12 については触れず、GPUの利用方法を中心に解説する。 13 この講義のもうひとつの視点は、CGに限らない一般のデータ並列計算である。大量の定 14 型的な計算をGPU上で行う方法が検討され始め、現在ではそれ専用のプログラミング環 15

境が適用されている。これを一般にGPGPU(General-Purpose computing on Graphics

16 Processing Units)と呼ぶ。本講では、まず、テクスチャを用いたラスタライズ法を一般 17 計算に拡張する方法を紹介する。GPUが高速計算と結びつく理由がわかるはずです。次 18 にGPGPUの専用環境として開発されたOpenCLを取り上げて解説する。名前からも推 19 察されるようにOpenCLとOpenGLは密接に関連している。その辺りについても言及す 20 る予定である。 21 この授業では、WindowsおよびMacOSで実際に動作するプログラムを示しながら、解 22 説を進めていく。プログラムは、適宜、Live Campusにアップロードする予定である。 23 また、講義担当者自身がOpenGLを学ぶに当たり、和歌山大学システム工学部デザイ 24

1誤って”Graphical Processing Unit”google検索を掛けたところ、意外にも15万件がヒットした。い ろんな論文でもこの術語が使用されているようだ。”Graphics Processing Unit”では51万件のヒットであっ たから、比率を考えると”Graphical ...”という呼び方も間違いではないのかもしれない。

(2)

ン情報学科床井研究室のホームページ2を参考にした。そのため、この講義テキストで示 1 すプログラムの構造やプログラム片がそのホームページの内容に類似しているものがある 2 ことを事前に告知する。プログラムをそのまま利用した箇所ではその旨を記載する。 3 ホームページを通じて貴重な情報をご教示いただいたことを、床井浩平先生に感謝いた 4 します。 5

(3)

フィックス

この章ではOpenGL/GLSLを用いたCGプログラムについて解説する。

2

2.1

プログラムの概要

3

GPUを用いるプログラムは、CPU上で動作するホストプログラムとGPU上で動作す

4

るシェーダープログラムからなる。片方だけでは動かない1

5

ホストプログラムはGPUを制御するためのプログラムであり、通常のプログラム実行

6

と同様にCPU上で動作する。この授業ではその制御にグラフィックスAPI(Application

7

Program Interface)として最もよく普及しているOpenGLを用いる。

8 OpenGLは、かつて最先端のCG専用コンピュータの開発・販売を行っていたシリコ 9 ン・グラフィックス社が社内用グラフィックス・ライブラリの仕様を1993年に公開したも 10 のである。仕様の抽象度が高く、様々なグラフィックス装置への移植が比較的容易であっ 11 たため、普及が進んだ。抽象度の高さは逆に個々のグラフィックス装置の性能を引き出す 12 ことを阻害する要因ともなるため、たとえばマイクロソフトのDirectXを利用した場合に 13 比べ、描画速度がやや遅いという欠点もある2,3 14

GLSLは、OpenGL Shading Languageの略であり、OpenGLと対になってGPU上の

15

プログラムを記述するプログラミング言語である。1990年代の公開された当初のOpenGL

16

にはGPUを操作するAPIは用意されていなかったが、急速に開発が進むGPUを効果的

17 に利用するために、OpenGLからほぼ10年遅れて仕様が策定され、GPUを高水準プログ 18 ラミング言語レベルで利用できるようになった。 19 1古いOpenGLではシェーダープログラムを作成しない場合、システムが用意する固定機能のシェーダー プログラムが自動的に仮定された。よって、初心者はシェーダーを意識することなく、CGプログラムを作る ことができたが、現在のOpenGLは処理の多様性、高速性を強く意識するため、ユーザが必ずシェーダプロ グラムを用意する必要がある。結果としてOpenGLは初心者には難易度が高いものになっている。 2DirectXはマイクロソフトがWindowsに提供するものであるため、マクロソフト自身が精力的にサポー トしている点も大きい。それに対して、OpengLのサポートはおざなりになりがちである。

3最近では、最新のGPUを想定し、かなり低レベルの操作ができるグラフィックスAPUとして、Vulkan

(4)

ホストプログラム バーテックス シェーダー プログラム フラグメント シェーダー プログラム 制御 制御、 入力データ 画像出力

CPU

GPU

ラスタ ライザ 図2.1: 3種類のプログラムの関係 GLSLのプログラム(以下、シェーダープログラム)はOpenGLを用いるホストプログ 1 ラムの管理下で実行される。GPUはCGの様々な処理をパイプライン的に実行しており、 2 パイプラインの各ステージ毎にシェーダープログラムを作成する必要がある。この授業で 3 用いるシェーダープログラムは、バーテックスシェーダプログラムとフラグメントシェー 4 ダープログラムの2種類である。この二つがライスタライザを挟んで3段のパイプライン 5 を成している(図2.1)。詳細は後に述べる。 6 結局、OpenGL/GLSLでCG描画を行う場合、プログラマは少なくとも3種類のプロ 7 グラム 8 1. ホストプログラム 9 2. バーテックスシェーダープログラム 10 3. フラグメントシェーダープログラム 11 を書かねばならない。 12 実は、最近のCG描画では図2.1のような単純な構成では不十分であり、これにジオメ 13 トリシェーダー、テッセレータなどのプログラムを追加できる(追加は必須ではない)。ま 14 た、最近では計算に特化した計算シェーダープログラムも利用でき、これがこの授業の後 15 半で述べるOpenCLに密接に関連している。

(5)

2.1.1 OpenGL の概要:ホストプログラムの構造 1 OpenGLについて注意すべき点は、OpenGLはグラフィックス処理のコア部分—たと 2 えば画面上に線分を引く、三角形を描画する — などの処理に関する仕様であって、コア 3 に付随する処理 — たとえばPCのディスプレイ上にウインドウを開く、ウインドウを閉 4 じる、タイマー割り込みを用いる、マウス入力を行う– などの処理はOpenGLの仕様に 5 は含まれない。 6 コア部分にOpenGLという統一仕様があるとしても、コア以外の部分はPCの種類と搭 7 載OSに強く依存するため、その仕様をひとつに決めることが難しい。たとえば本格的な 8 CGソフトウェアを開発する場合、そのソフトウェアの操作は搭載OSのウインドウシステ 9 ムの機能を利用し、そのOS操作感に合うように作るべきである。その場合、搭載OS独 10 自の処理を細かく実装する必要があり、他の機種の他のOS上で同じプログラムが動くこ 11 とはほとんど期待できない。しかし、もし操作性にあまりこだわらず、手早くソフトウェ 12 アを試作したいだけならば、コア以外については様々なPC、OSに共通する基本的な操作 13

のみを提供すればよいだろう。そのような目的のAPIとして GLUT(OpenGL Utility

14 Toolkit)がしばしば利用されてきた。この授業でもGLUTを用いる。CGの学習という 15 目的ではそれで十分である。 16 GLUTはOpenGLの仕様が公開された時にほとんど時を同じくして提案された。面倒 17 なウインドウの生成などを簡単な関数呼び出しで実現できる。また描画の無限ループをサ 18 ポートする関数が用意されており、APIというよりも、プログラム全体の構造を決めるフ 19 レームワークを提供している。なお、GLUTは既に開発が終了して久しいライブラリであ 20 り、MacOSにおいては「deprecated」(サポートが廃止される可能性があり、使用を推奨 21 しない)と警告されている。早晩利用できなくなると思われるが、現時点では新しいAPI 22 (たとえばGLFW4など)に乗り換える労力が決して小さくない 特に授業で学ぶため 23 だけのために面倒なインストール作業そして授業終了後のアンインストール作業を行うこ 24 とはあまり効果的ではない —から、この授業ではGLUTを使用する。 25 補足: OpenGLで定義されている関数の名前には必ずglという接頭語が付く。同様に、 26 GLUTで定義されている関数の名前には必ずglut という接頭語が付く。 27 OpenGLを用いるホストプログラムの一般的な処理手順と処理内容は以下の通りである。 28 実行時環境の初期化: まず、OpenGL/GLUTライブラリの定型的な初期化、描画ウイン 29 4講義担当者はOpenGLを使う研究用プログラムでは既にGLUTからGLFWに乗り換えた。

(6)

ドウの生成などを行う必要がある。 1 シェーダー実行可能プログラムの準備: シェーダープログラムのコンパイル・リンク、GPU 2 への登録を行う。 3 シェーダーへの入力データの準備: 実際に描画する被写体のデータをホストプログラム 4 からGPUへ入力するなどの準備を行う。 5 シェーダー実行可能プログラムの起動: シェーダー実行可能プログラムを起動する。 6 補足: 上記「シェーダー実行可能プログラムの準備」についてやや長い補足解説を行う。 7 まず、プログラムには、ソースプログラム(ソースコード)、オブジェクトプログラム 8 (オブジェクトコード)、実行可能プログラム(実行可能コード)の三種類があることを思 9 い出そう。ここまで「シェーダープログラム」と呼んでいたものは暗にソースプログラム 10 のことを指していた。これ以降、混乱を避ける場合には「シェーダーソースプログラム」 11 という呼ぶ方も用いることとする。それをコンパイルしたものを「シェーダーオブジェク 12 トプログラム」と呼ぶこととする。さらに、それを(それらを)リンクしたものを「シェー 13 ダー実行可能プログラム」と呼ぶこととする。 14 さて、現在、シェーダーの機械語の仕様は、それがハードウェアに近い低レベルのもので 15 あれ、抽象度の高いレベルであれ、統一の仕様は策定されていない。そのため、それぞれ 16 のGPUのための専用コンパイラはそれぞれのデバイスドライバと対になって供給されて 17 いる状況である。その結果、シェーダーソースプログラムのコンパイルは、Visual Studio 18 や Xcodeのような統合開発環境に組み込まれて実行されるのではなく、ホストプログラ 19 ムから OpenGLのコンパイラを関数呼び出しする簡易的な方式で実装されている。この 20 方式の問題点は以下の通りである。 21 1. アプリケーションプログラム実行の中でコンパイルされるため、アプリケーション 22 プログラムの立ち上がりが総じて遅くなる。たとえばゲームならば、実際にゲーム 23 がスタートするまでにユーザは多少待たされることになり、好ましくない。 24 2. 上記のユーザーの待ち時間のために、コンパイラは高度なコード最適化処理5を実施 25 できない。もしシェーダーソースプログラムを事前にコンパイルできるならば、十 26 分な時間(必要ならば数十分、数時間)を掛けて高度で複雑な最適化処理を実施で 27 きる6 28

(7)

3. シェーダーソースプログラムをコンパイルして生成される機械語コードを見る(読 1 む)ことができない7。よって、ユーザーは実際にGPU上でどのような機械語コー 2 ドが実行されるのか知ることができず、シェーダーソースプログラムを書き直すな 3 どのユーザレベルのプログラム最適化が難しい。 4 実行時コンパイルのこれらの問題点は広く認識されているが、全面的な解決には至ってい 5 ない。 6 OpenGLの仕様はプログラミング言語とは独立しており、様々なプログラミング言語か 7 ら利用可能である。この授業では最も一般的なC++を用いる。 8 2.1.2 OpenGL の概要:シェーダープログラムの構造 9 既に述べたように、ユーザは少なくとも2種類のシェーダーソースプログラムを作成す 10 る必要がある。それらはそれぞれのシェーダ上で動作し、以下のような役割を担っている。 11

バーテックス・シェーダー(vertex shader) ... 頂点(vertex)シェーダーともいう。

12 3D CGでは主に頂点の座標変換を行うため、この名称が付いている。実際には座標 13 変換に限らず、自由な計算ができるため、アイディア次第ではGPUの面白い使い方 14 ができる。GPGPUなどもそこから始まっている。 15  ホストプログラムからバーテックスシェーダーへの入力は、各頂点の位置、色、そ 16 の他の付随情報であり、ユーザが自由に設計できる。たとえば線分は2端点で定義 17 され、三角形は3頂点で定義される。バーテックスシェーダーはそれらのそれぞれ 18 の頂点に関する計算を行う。 19  計算結果はバーテックスシェーダーから出力され、ラスタライザに入力される。 20

フラグメント・シェーダー(fragment shader) DirectXではピクセル(pixel=画素)

21 シェーダと呼ぶ。主にラスタライズで求められた各画素の色の計算を行う。 22  バーテックスシェーダーから出力された頂点情報は、ラスタライザを経由して各 23 画素毎の情報へ線形補間されて、フラグメントシェーダーに入力される。フラグメ 24 ントシェーダーではその入力情報を元に画素の色(と必要ならば深度情報)を計算 25 する。色(と深度情報)は画像バッファ上の対応する画素へ書き込まれる。 26 シェーダープログラムはGLSLで記述するが、GLSLはC言語とほとんど同じ基本構文 27 を持つ言語であるから、C言語に慣れている者にはプログラミングのストレスがほとんど 28 7リバースエンジニアリングを用いれば可能と思われるが、法的に問題があり、やるべきではない。

(8)

✓ ✏ void display() { /* ここに描画処理 */ } int main() { /* ここに初期化処理 */ glutDisplayFunc(display); // 描画関数をcallback関数として登録 glutMainLoop(); // 無限ループで処理を繰り返すことの設定 return 0; } ✒ ✑ 図2.2: ホストプログラムのひな形(その1) ない。C言語と異なるのは、データの入出力に関連する特殊なルール、ベクトルデータ型 1 の拡張等で付加されている点である。詳細は後に例題を用いて解説する。 2 2.1.3 OpenGL/GLSL プログラムの外形 3 GLUTを用いたホストプログラムの最も単純な形は図2.2の通りである。GLUTでは 4 コールバック(callback)を用いて描画を行なう。まず、関数呼び出し 5 glutDisplayFunc(display)8 6 によって、関数display() をGLUTの実行時管理テーブルへ登録しておく。次に、関数 7 呼び出し 8 glutMainLoop()9 9 によってプログラムはGLUTのイベント処理ループへ入る。このループでは、GLUTの 10 実行時システムがウインドウの描画/再描画が必要と判断したとき(たとえばプログラム 11 開始直後、描画ウインドウの状態が変化したとき)にglutDisplayFunc()によって事前 12 に登録したコールバック関数(すなわち、display())を自動的に呼び出すようになって 13 いる。よって描画のタイミングをユーザがいちいち管理する必要はなく、プログラムを容 14

(9)

✓ ✏ void initSystem() { /* ここにシステムパラメータの設定等の初期化処理 */ } void initData() { /* ここに被写体データなどの初期化処理 */ } void display() { /* ここに描画処理 */ } int main() { initSystem(); initData(); glutDisplayFunc(display); // 描画関数をcallback関数として登録 glutMainLoop(); // 無限ループで処理を繰り返すことの設定 return 0; } ✒ ✑ 図2.3: ホストプログラムのひな形(その2) 易に作成できるようになっている(そもそも管理を一般ユーザに任せるのは難易度が高す 1 ぎる)。 2 上のひな形は一見すると分かりやすいが、実際には初期化処理をmain関数にベタ書き 3 する形になっており、プログラム作法上あまり好ましくない。そこでシステム関連の初期 4 化を行う関数initSystem() と被写体データの初期化を行う関数initData()を定義し、 5 ひな形を図2.3のように修正する。この授業では一貫してこの形のプログラムを用いるこ 6 ととする。具体例は次節以降で解説する。 7

2.2

簡単な線画:最も単純なプログラム

8 ようやく最初の例題である。この節で作成するCG画像は、×印を二本の線分で描く 9 図2.4である。この単純な画像を作成するプログラムをできるだけ丁寧に紹介していく。 10 こんな単純な線画を描画するだけのためにうんざりするような量のプログラムを書か 11 ねばならないことに驚くだろうが、高度なグラフィックス処理には避けて通れないと理 12

(10)

図2.4: 画像例(その1)

解してほしい。OpenGLだけではなく、DirectX、Metalなども同様である。例外として、

1 Unityは、細かいところへ手が入りにくいももの比較的容易なプログラミングができるよ 2 うだ(講義担当者自身が本格的に使ったことがないので、伝聞の域を出ないが)。 3 2.2.1 ホストプログラム 4 図2.5∼図2.8、図2.10、図2.11はホストプログラムである。これらのプログラムをこ 5 の順番にひとつのテキストファイルにまとめれば、図2.3の形のプログラムになる。 6 なお、ここに示すプログラムでは、プログラムの本質的な流れを理解することを目的と 7 するため、処理に伴うエラー検出/エラー処理を全て省略している。実際のプログラムに 8 おいてエラー検出/エラー処理は必須であることは言うまでもない。これについてはプロ 9 グラムを一通り解説した後で再度触れる。 10 図2.5はヘッダーファイルのインクルード、関数のプロトタイプ宣言である。OpenGL関 11 連のインクルード・ファイルは、和歌山大学床井研究室のホームページをそのまま用いた。 12 図2.6はシステムの初期化部である。以下、順に解説する。 13 1. まず、図2.6の冒頭に大域変数 14

(11)

✓ ✏ #include <iostream> using namespace std; #include <stdio.h> #include <stdlib.h> #if defined(WIN32)

# pragma comment(lib, "glew32.lib") # include "glew.h"

# include "glut.h" # include "glext.h"

#elif defined(__APPLE__) || defined(MACOSX) # include <GLUT/glut.h>

#else

# define GL_GLEXT_PROTOTYPES # include <GL/glut.h>

#endif

void initSystem(int argc, char *argv[]); void initData();

void display(void);

GLuint compileProgram(GLenum type, const GLchar *file);

✒ ✑ 図2.5: ×印を描画するホストプログラム(その1、その2へ続く) を宣言する。この programは、コンパイル&リンク後のシェーダー実行可能プログ 1 ラムの参照番号を保持するための変数である。具体的な参照番号を知る必要はない。 2 ホストプログラムの他の関数から参照されるため、大域変数とする。なお、ここで 3

は参照番号と呼んでいるが、OpenGLの原語では“name of a program object”であ

4 る。ここでは“name”を参照番号と言い換える。 5 2. 関数呼び出し 6 glutInit(&argc, argv)10 7

はGLUTライブラリの初期化を行う。引数 int argc, char *argv[]は実際には

8

不要なのだが、慣例上、そのまま受け渡している。

9

3. 関数呼び出し

10

glutInitDisplayMode(GLUT RGBA|GLUT SINGLE)11 11

10void glutInit(int *argcp, char **argv); 11void glutInitDisplayMode(unsigned int mode);

(12)

✓ ✏ GLuint program;

void initSystem(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGB|GLUT_SINGLE); glutInitWindowSize(512,512); glutCreateWindow("Test Window"); glClearColor(0.5,0.5,0.5,1); #if defined(WIN32) glewInit(); #endif

GLuint vs = compileProgram(GL_VERTEX_SHADER, "shader.vert"); GLuint fs = compileProgram(GL_FRAGMENT_SHADER, "shader.frag"); program = glCreateProgram(); glAttachShader(program, vs); glAttachShader(program, fs); glLinkProgram(program); glUseProgram(program); } ✒ ✑ 図2.6: ×印を描画するホストプログラム(その2、その3へ続く) は、生成するウインドウの表示設定である。GLUT RGBAは画像をフルカラーで表示 1 することを意味し、GLUT SINGLEは画像描画用フレームバッファを1枚だけ使用す 2 ることを意味する。ともにデフォルトの設定なので、実はこの関数呼び出しは不要 3

である。CGアニメーションを行う場合にはGLUT SINGLEの代わりにGLUT DOUBLE

4 を指定する。 5 4. 関数呼び出し 6 glutInitWindowSize(512,512)12 7 は、生成するウインドウのサイズを512画素× 512画素 に設定している。 8 5. 関数呼び出し 9

(13)

glutCreateWindow("Test Window")13 1 は、”Test Window”というラベル名の付いたウインドウをコンピュータディスプレ 2 イ上に生成する。 3 6. 関数呼び出し 4 glClearColor(0.5,0.5,0.5,1)14 5 は、画像の背景色、アルファ値(透明度)を(r, g, b, a) = (0.5, 0.5, 0.5, 1)(灰色、図 6 2.4参照)に設定する。この関数は設定のみを行い、背景色によるウインドウの塗り 7 つぶしは図2.10のglClear()によって行うことを注意する。 8

7. 関数呼び出し glewInit(); はWindowsに必要である。MacOSでは不要である。

9 glewは拡張ライブラリである。このライブラリの関数はここでは用いないが、この 10 初期化を削除するとWindowsでの実行が正常に行われないため残しておく。 11 8. 関数呼び出しと代入 12

GLuint vs = compileProgram(GL VERTEX SHADER, "shader.vert") 13 は、バーテックスシェーダープログラムのコンパイル処理である。関数compileProgram() 14 は図2.7に定義している通りだが、処理内容が単純ではないため、詳細は後に解説 15 する。この関数では第一引数にシェーダーの種類をOpenGLの定義する定数で指定 16 する。バーテックスシェーダーの場合にはGL VERTEX SHADERである。第二引数で 17 シェーダープログラムを格納するテキストファイル名を指定する。この授業ではバー 18 テックスシェーダーのプログラムは”shader.vert”に格納すると約束する15。その 19 内容は図2.12の通りである。関数の戻り値はコンパイルされたシェーダーオブジェ 20 クトプログラムの参照番号である。具体的な参照番号を知る必要はない。 21 9. 関数呼び出しと代入 22

13int glutCreateWindow(char *name); nameというラベルのウインドウを生成し、現在のターゲット

ウインドウをこのウインドウに設定する。int型の戻り値はこのウインドウの参照番号である。ひとつのプ

ログラムで複数のウインドウに画像描画するときにこの参照番号を利用し、ウインドウを切り替える。ウイ ンドウを切り替える関数は void glutSetWindow(int win); である。

14

void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); な お 、 GLfloatはfloatに別名をつけたものと考えてよい。

(14)

GLuint fs = compileProgram(GL FRAGMENT SHADER, "shader.frag") 1 はフラグメントシェーダープログラムのコンパイル処理である。第一引数値がバー 2 テックスシェーダーの場合と異なることに注意。 3 10. 関数呼び出しと代入 4 program = glCreateProgram()16 5 は、空のプログラムを生成し、その参照番号を大域変数programに格納する。具体 6 的な参照番号を知る必要はない。 7 11. 関数呼び出し 8 glAttachShader(program, vs)17 9 glAttachShader(program, fs) 10 は、シェーダーオブジェクトプログラムvs、fs をprogramにアタッチする(貼り 11 付ける)。 12 12. 関数呼び出し 13 glLinkProgram(program)18 14 は、program にアタッチされたシェーダーオブジェクトプログラム間の入出力変数 15 の矛盾などをチェックし、GPUでシェーダー実行可能プログラムに仕上げる。 16 13. 関数呼び出し 17 glUseProgram(program)19 18 は、参照番号programのシェーダー実行可能プログラムをこれから実行することを 19 宣言している。システムはこのプログラムをGPU内にセットアップする。 20 上で説明を省略した関数compileProgram()は、図2.7のように用意する。次にこれを 21 解説する。 22 16GLuint glCreateProgram(void);

(15)

✓ ✏ GLuint compileProgram(GLenum type, const GLchar *file)

{

FILE* fp = fopen(file, "r"); #define MAX_SHADER_FILE_SIZE 10000

GLchar ftext[MAX_SHADER_FILE_SIZE];

unsigned long n = fread(ftext, 1, MAX_SHADER_FILE_SIZE, fp); ftext[n] = ’\0’;

fclose(fp);

GLuint shader = glCreateShader(type); const GLchar* ftext_handle = ftext;

glShaderSource(shader, 1, &ftext_handle, NULL); glCompileShader(shader); return shader; } ✒ ✑ 図2.7: ×印を描画するホストプログラム(その3、その4へ続く) 1. 関数本体の前半部分: 1 FILE* fp = fopen(file, "r"); 2 #define MAX_SHADER_FILE_SIZE 10000 3 GLchar ftext[MAX_SHADER_FILE_SIZE]; 4

unsigned long n = fread(ftext, 1, MAX_SHADER_FILE_SIZE, fp); 5 ftext[n] = ’\0’; 6 fclose(fp); 7 は、ファイルの内容を文字配列 ftextに格納する。ただし、このプログラムは実装 8 を端折っており、シェーダーファイルのサイズが10KBを超える(簡単なプログラ 9 ムではまず超えないが)とプログラムがメモリ例外を起こす。 10 2. 関数呼び出しと代入 11

GLuint shader = glCreateShader(type)20 12

では、第1引数の型(ここではGL VERTEX SHADERまたはGL FRAGMENT SHADER)の、

13

空のシェーダーオブジェクトプログラムを生成する。

14

3. 代入

15

const GLchar* ftext handle = ftext; 16

(16)

は、シェーダーソースプログラムが格納されている配列の先頭アドレスftextを一 1 旦、変数 ftext handleに格納する。アドレスを格納する変数をハンドルと呼ぶ。 2 4. 関数呼び出し 3

glShaderSource(shader, 1, &ftext handle, NULL)21 4 は、ソースファイルの文字列をシェーダオブジェクトプログラムへ格納する。 5 5. 関数呼び出し 6 glCompileShader(shader);22 7 は、シェーダオブジェクトプログラムに事前に格納されているシェーダーソースプ 8 ログラムをコンパイルする。ソースプログラムに構文エラーがある場合には、本来 9 はここでそのエラーを表示し、プログラムの実行を強制中断すべきである。しかし、 10 先に述べたように、この解説ではとりあえずのプログラム理解を優先するため、エ 11 ラー検出/エラー処理は全て取り除いている。 12 6. 最後にコンパイル済みのシェーダオブジェクトプログラムの参照番号を戻り値とする。 13 以上でライブラリの初期化、実行可能なシェーダープログラムの準備ができた。 14 ここではシェーダープログラムを一組だけ用意したが、実際のCG画像では様々な被写 15 体を様々な手法で描画するため、複数のシェーダープログラムを用意するのが通常である。 16 その場合、実行するシェーダープログラムを切り替えながら、描画を繰り返すこととなる。 17 図2.8では、描画データを準備する。ここで考えている例題では、二本の線分で×印を 18 描くだけ(図2.4参照)であるから、処理は比較的単純である。以下、順に解説する。 19 1. 大域定数宣言 20

const int NUM POINTS = 4; 21

では、2本の線分の両端点のために、計4個の頂点を使用することを宣言する。こ

22

の値は描画関数 display() (図2.10)でも参照するため、大域化しておく。

(17)

✓ ✏ const int NUM_POINTS = 4; //頂点の数

void initData() { float pos[2*NUM_POINTS]; pos[0] = -0.5; pos[1] = -0.5; pos[2] = +0.5; pos[3] = +0.5; pos[4] = +0.5; pos[5] = -0.5; pos[6] = -0.5; pos[7] = +0.5; GLuint bufID; //参照番号 glGenBuffers(1, &bufID); glBindBuffer(GL_ARRAY_BUFFER, bufID);

glBufferData(GL_ARRAY_BUFFER, sizeof(float)*2*NUM_POINTS, pos, GL_STATIC_DRAW);

GLint p = glGetAttribLocation(program,"position"); glVertexAttribPointer(p, 2, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(p); glLineWidth(10); } ✒ ✑ 図2.8: ×印を描画するホストプログラム(その4、その5へ続く) 2. 関数 initData()の冒頭の配列宣言 1

float pos[2*NUM POINTS]; 2 で描画に必要な浮動小数点数値を格納する配列領域を確保する。なお、この例題で 3 は2次元座標値のみを扱うから、ひとつの頂点座標を保持するにはfloat型が2個 4 必要である。頂点の個数は NUM POINTSである。よって、必要とされる配列要素数 5 は2*NUM POINTSである。 6

注意:GPUでは浮動小数点数値は倍精度(double型)ではなく、単精度(float

7 型)で表現するのが一般的である23。何故ならば、CG画像の画素数は縦横せいぜ 8 い数千画素である。各画素の輝度値は一般にRGBそれぞれ256段階である。とな 9 れば、CG画像の数値誤差が人の目に気にならないためには、GPUの計算に用いる 10 数値の精度は10進数4桁程度が必要である。まるめ誤差の発生等を考慮しても精度 11

23最近ではNVIDIA Telsaのように、GPGPUのために倍精度計算を強化したGPUも開発・販売されて いる。

(18)

は10進数6桁もあればおおむね足りるだろう。10進数6桁の数値は2進数20桁程 1 度に相当し、ならば仮数部が23bitのfloat型で十分である。 2 3. 配列中にデータを並べる順番には以下のルールがある。 3 (a) 2次元座標値は、以下の図のように、x座標値、y座標値をこの順番で24配列 4 要素に連続して格納する。 5 ... x座標値 y座標値 ... 6 (b) 線分を表すときには、以下の図のように、始点の2次元座標値、終点の2次元 7 座標値を配列要素に連続して格納する。 8 ... 始点の2次元座標値 終点の2次元座標値 ... 9 よって、図2.8のプログラムでは、配列要素への代入文の1行目: 10 pos[0] = -0.5; pos[1] = -0.5; 11 が1本目の線分の始点の2次元座標値 (x, y) = (−0.5, −0.5)を表し、同様に、2行 12 目が1本目の線分の終点の2次元座標値、3行目が2本目の線分の始点の2次元座標 13 値...と座標値を格納している。 14

4. GLuint bufID;からglEnableVertexAttribArray(p);までの7行は、配列 pos 15 に設定した値をGPUに転送し、かつその値をバーテックスシェーダープログラム 16 とリンクする処理である。この処理手順を図2.9 を用いて説明する。実はこの辺の 17 内容は非常に分かりにくく、しばしば「何故、OpenGL はこのように複雑でわ 18 かりにくい仕様を採用しているのか?」と批判されているところである。あまり 19 悩まず、さっさと次へ行く程度の軽い気持ちで臨むのがよい。 20

(a) OpenGLは、プログラムの実行に関わる様々な情報をOpenGLコンテキスト

21

として一元管理している。GPUのメモリの使い方もその管理の対象になって

22

いる。GPUのメモリにはバッファオブジェクト(Buffer Object)を設けるこ

23

とができて、それぞれのバッファオブジェクトにはそれが占有するメモリ領域

24

を設定できる。関数呼び出し

(19)

メモリ

CPU

GPU

頂点データ(pos) OpenGL コンテキスト VBO メモリ バーテックスシェーダープログラム ...

attribute vec2 position; ... (c) (e) (d) (c) (a), (b)

図2.9: GPUメモリへの頂点データの転送:(a) OpenGLコンテキストにバッファオブジェ クト bufID を生成する。(b) bufIDをVertex Buffer Objectとみなし、設定対象とする。

(c) bufIDに対応するGPUのメモリ領域を確保し、CPUのメモリからデータを転送する。

(d) シェーダープログラムから特定のattribue変数の場所 pを求める。(e) bufIDと p

を結合する。 glGenBuffers(1, &bufID)25 1 は、OpenGLコンテキスト内にひとつのバッファオブジェクトを新たに生成す 2 る操作である。図2.9で言えば、右上の表にひとつ項目が追加される。生成され 3 たバッファオブジェクトの参照番号が変数budIDに格納される。具体的な参照 4 番号を知る必要はない。OpenGLでは、この参照番号をしばしば名前(name) 5 と呼ぶが、実態は GLuint(=符号なし整数)であるから、このテキストでは 6 参照番号という言い方をする。 7 (b) バッファオブジェクトには、その用途によって様々な種類があり、種類毎に設定 8 する方法が異なる。悩ましいことに、OpenGLでは同じ種類の複数のバッファ 9 オブジェクトを同時に設定できない、つまり、一時にはひとつのバッファオブ 10 ジェクトしか設定の対象にできない、という不便な仕様になっている。関数呼 11 び出し 12

25void glGenBuffers(GLsizei n, GLuint * buffers); 1回の関数呼び出しによってn個のバッファ

(20)

glBindBuffer(GL ARRAY BUFFER, bufID)26 1

は、参照番号bufIDのバッファオブジェクトについて、それをGL ARRAY BUFFER

2

という種類のバッファとしてこれ以降の設定の対象にすることを指定している。

3

GL ARRAY BUFFERは、バッファがバーテックスシェーダーへの入力バッファで

4

あることを意味し、その種類のバッファをVertex Buffer Object(VBO)と呼

5

んでいる。

6

(c) 関数呼び出し

7

glBufferData(GL ARRAY BUFFER, sizeof(float)*2*NUM POINTS, pos, 8 GL STATIC DRAW)27 9 は、以下の処理をまとめて行う関数である。 10 i. 第1引数GL ARRAY BUFFERは、この関数呼び出しで対象とするバッファの 11 種類を指定している。直前の関数呼び出しglBindBuffer()において、参 12

照番号bufID がGL ARRAY BUFFERであると設定しているため、bufIDが

13

この関数呼び出しの対象である(分かりにくい!)。

14

ii. 第2引数 sizeof(float)*2*NUM POINTS はバッファのサイズをバイトの

15

単位で指定している。指定されたサイズのメモリ領域がGPUのメモリに

16

確保される。

17

iii. 第3引数posは、確保されたGPUのメモリにCPUのメモリから転送す

18 るデータ(この場合、配列データ)の先頭アドレスを指定している。転送 19 するデータのサイズは第2引数の値である。第3引数にNULL を指定する 20 と、データの転送は行わない。 21  なお、CPUのメモリ上のデータをGPUのメモリへ転送する方法は他に 22 もある28のだが、初学者には glBufferData() を用いる方法が最も単純 23 で変な誤解が生じにくいと思われる。 24

iv. 第4引数 GL STATIC DRAWは、確保するGPUのメモリがシェーダープロ

25

グラム実行時には主にデータの読み出しに利用されることを宣言している。

26

この宣言を間違えてもシェーダープログラムは正常に動作する(たぶん)

27

26void glBindBuffer(GLenum target, GLuint buffer); targetには14 種類(最新バージョンの OpenGLの場合)あるが、詳細は省略する。

(21)

が、データの読み出しに無駄な時間が掛かる場合がある。引数として、他

1

にGL STREAM DRAW、GL STATIC COPY、GL DYNAMIC READなど、計9種類

2 が指定できる。 3 (d) 作業はまだ終わらない。次に、上記の関数呼び出しでGPU上に確保されたメ 4 モリ領域をシェーダープログラム(図2.12)と結合する。 5  関数呼び出しと代入 6 GLint p = glGetAttribLocation(program,"position");29 7 は、第1引数 program に指定されたシェーダープログラム(正確には既にコ 8 ンパイル&リンクされたシェーダー実行可能プログラムの参照番号)において 9 attribute宣言された変数の中で変数名が第2引数の文字列"position"と同 10 じものを探し、その場所(location)を変数pへ代入する。実際、図2.12には 11

attribute vec2 position; 12

という宣言がある。もしその変数名が見つからない場合にはエラーとなる。

13

(e) 関数呼び出し

14

glVertexAttribPointer(p, 2, GL FLOAT, GL FALSE, 0, 0)30 15 は、次の二つの処理を行う。 16 i. 第1引数pで場所を指定したシェーダー実行可能プログラム中のarrribute 17 変数(直前のglGetAttribLocation()で指定した"position"という名前 18 の変数)と現時点で設定対象となっているVBO(glBindBuffer()で指定 19 した参照番号bufIDのバッファオブジェクト)を結合する。これによって、 20 シェーダープログラムが実行されるときには、VBOの先頭から順にデー 21 タが取り出され、それがそれぞれのarrribute変数に代入されることに 22 なる。 23 ii. 第2引数から第6引数は、VBOの先頭から順にデータが取り出す方法を 24 指定する。 25 A. 第2引数2は、対応するarrribute変数が2個のコンポーネント(基 26 本構成要素)を持つことを意味する。他に1,3,4の整数値が指定可能で 27 ある。 28 29

GLint glGetAttribLocation(GLuint program, const GLchar *name);

30void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer);

(22)

B. 第3引数 GL FLOAT は、前出のコンポーネントがfloat型であること 1

を意味する。他に、GL BYTE、GL UNSIGNED BYTE、GL SHORT、

2

GL UNSIGNED SHORT、GL INT、GL UNSIGNED INTなどが指定可能であ 3

る。

4

 図2.12の宣言

5

attribute vec2 position; 6

では変数positionはvec2型である。この型はfloat型2個からな

7 るベクトル型を意味し、第2、第3引数と正しくマッチしている。当然 8 だが、マッチしていない引数を指定すると、シェーダープログラムが 9 正しく動作しない。 10 C. 第4、第5、第6引数の説明は省略する。高度に凝った処理を行わない 11 限り、GL FALSE, 0, 0でよい。 12 (f) 関数呼び出し 13 glEnableVertexAttribArray(p)31 14 は、第1引数pで場所を指定したシェーダー実行可能プログラム中のarrribute 15 変数を利用可能にする。 16 (g) 関数呼び出し 17 glLineWidth(10)32 18 は、線分を描く場合の線の太さを指定する。デフォルトでは1.0である。この 19 関数呼び出しはむしろ描画関数display() に置くかもしれないが、今回の例 20 題では一度設定した線分の太さを変えないため、initData()内に置いた。 21 以上で、入力データの準備ができた。 22 描画関数display()は図2.10の通りである。以下、順に解説する。 23 1. 関数呼び出し 24 glUseProgram(program) 25 は既に関数initSystem() 内で呼び出したが、描画関数内でも 26

(23)

✓ ✏ void display(void)//実際の描画を行なう関数 { glUseProgram(program); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_LINES,0,NUM_POINTS); glFlush(); } ✒ ✑ 図2.10: ×印を描画するホストプログラム(その5、その6へ続く) 2. 関数呼び出し 1

glClear(GL COLOR BUFFER BIT)33 2

は、画像バッファを事前に設定した色(図2.6のglClearColor(0.5,0.5,0.5,1))

3

でクリアリセットする。実引数GL COLOR BUFFER BITは画像のみをリセットするこ

4

とを意味する。3D CGの場合には、デプス・バッファのリセットも必要である。

5

3. 関数呼び出し

6

glDrawArrays(GL LINES,0,NUM POINTS)34 7 は、直前のglUseProgram(program) で指定したシェーダー実行可能プログラムを 8 起動する。引数の意味は以下の通り。 9 (a) 第一引数には描画する被写体の形状を指定する。たとえば 10 ポイントスプライトの場合 ... GL POINTSを指定する。 11

線分の場合 ... GL LINES, GL LINE STRIP, GL LINE LOOPなどを指定する。

12

詳細は省略

13

三角板の場合 ... GL TRIANGLES, GL TRIANGLE STRIP, GL TRIANGLE FANな

14 どを指定する。詳細は省略 15 ここでは単純な線分を用いるため、GL LINESを指定している。 16 (b) 第二引数には、頂点データの配列について、どの位置から描画に用いるか、先 17 頭位置を指定する。通常は関数initData()で設定した配列の先頭から用いる 18 だろうから 0 を指定する。しかし、少し凝ったプログラムでは0 以外の位置 19 を指定することもしばしばある。 20

33void glClear(GLbitfield mask);

(24)

✓ ✏ int main(int argc, char *argv[])

{ initSystem(argc,argv); initData(); glutDisplayFunc(display); glutMainLoop(); return 0; } ✒ ✑ 図2.11: ×印を描画するホストプログラム(その6、その5から続く) (c) 第二引数には、描画に用いる頂点の数を指定する。今回の例題では頂点数は 1 NUM POINTSであった(図2.8の1行目)。 2 4. 関数呼び出し 3 glFlush()35 4 は、glDrawArrays()による描画を強制する。というのも、実はglDrawArrays() 5 は描画を実行するというよりも描画を予約する関数であるため、glDrawArrays() 6 を呼んだからといって、実際に描画が開始されるとは限らない仕様になっているか 7 らである。そのような事態は複数台のPCがネットワークを介して描画処理を行う 8 ような場合以外はまず起きないが、確実に描画を行うためにglFlush()を呼ぶこと 9 がOpenGLの流儀になっている。 10 ホストプログラムの最後はmain関数である。図2.11の通りであるが、これについては 11 すでに図2.3で解説した。 12 2.2.2 バーテックスシェーダープログラム 13 現時点の最新のOpenGL/GLSLの仕様はバージョン4.6であり、様々な新機能が盛り込 14 まれている。しかしすでに述べたように、この授業ではそれよりもはるかに前のバージョ 15 ンの1.2を用いる。基本の理解にはそれでも十分である。 16 図2.12が例題のバーテックスシェーダープログラムのソースである。主要部分の解説は 17

(25)

✓ ✏ #version 120

attribute vec2 position; void main(void) { gl_Position = vec4(position,0.0,1.0); } ✒ ✑ 図2.12: ×印を描画するプログラム(バーテックスシェーダプログラム) 1. 1行目 1 #version 120 2 は、ここで用いるGLSLのバージョンが1.2であることを宣言している。 3 2. attribute変数の宣言 4

attribute vec2 position; 5 は、positionの値は、関数initData()で設定したバッファから供給されることを 6 意味している。よって、関数initData()の設定と矛盾すると、プログラムの動作 7 が正常に行われない。 8 3. main関数内の代入文 9 gl Position = vec4(position,0.0,1.0); 10 の左辺のvec4型の予約変数 gl Positionには、頂点のウインドウ上の座標値 (x, y, z, w) を格納すると約束されている。この座標値は同次座標系上の値であるため、通常の 3次元空間では (X, Y, Z) = (x w, y w, z w) と解釈される。そしてOpenGLでは頂点が −1.0 ≤ X ≤ 1.0, − 1.0 ≤ Y ≤ 1.0, − 1.0 ≤ Z ≤ 1.0, w > 0

(26)

✓ ✏ #version 120 void main(void) { gl_FragColor = vec4(1.0, 0.0, 0.0, 0.0); } ✒ ✑ 図2.13: ×印を描画するプログラム(フラグメントシェーダプログラム) を満たすときに限り描画されると約束されている。描画位置はウインドウを−1.0 ≤ 1 X ≤ 1.0、−1.0 ≤ Y ≤ 1.0の矩形領域とするときの位置(X, Y ) である。Zの値は 2 描画位置には用いられない。 3  いま、入力バッファから供給されたattribute変数positionのx座標値をpx、y 座標値を pyとするとき、上の式の右辺 vec4(position,0.0,1.0)は、 (x, y, z, w) = (px, py, 0.0, 1.0) を意味する(0.0, 1.0がpx, pyの後ろに付く)。よって、通常の3次元空間の座 標点としては (X, Y, Z) = (px, py, 0.0) と解釈される。つまり、 −1.0 ≤ px ≤ 1.0, − 1.0 ≤ py ≤ 1.0 ならば、この頂点はウインドウ上に描画されることになる。 4 2.2.3 フラグメントシェーダープログラム 5 図2.13が例題のフラゴメントシェーダープログラムのソースである。 6 main関数内の代入文 7 gl FragColor = vec4(1.0, 0.0, 0.0, 0.0); 8 の左辺のvec4型の予約変数 gl FragColor には、、その頂点の色を格納すると約束され 9 ている。代入文の右辺の値は、透明度 0 の赤色 (1.0, 0.0, 0.0, 0.0) であるから、線分は全 10 体に渡って赤になる(図2.4)。 11

(27)

V.S. F.S. V.S. ラスタ ライザ F.S. F.S. F.S. F.S. F.S. V.S. F.S. V.S. ラスタ ライザ F.S. F.S. F.S. F.S. F.S. 図2.14: 線画を描く場合のシェーダーの動作例 2.2.4 シェーダープログラムの実行の様子 1 図2.14はここまで解説してきたプログラムの、特にGPU内での実行の様子を図示した 2 ものである。 3 バーテックスシェーダーの動き この例題では、関数 initData() で4頂点の値を設定 4 している。それらの各頂点がバーテックスシェーダープログラムのattribute変数 5 positionへ供給され、四つのバーテックスシェーダープログラムが並列実行され 6 る。バーテックスシェーダー間のデータのやり取りは一切なく、全く独立に並列実 7 行される(実際には完全同時並列ではなく、GPUに搭載されたプロセッサ数によっ 8 て、一部直列実行されるだろうが、概念上は並列実行である)。 9 ラスタライザの動き バーテックスシェーダーで求められたgl Positionの値は、入力バッ 10 ファの先頭から順番に二つずつペアにされて、ウインドウ上の線分の始点と終点と 11 見なされる(glDrawArrays()関数の第一引数でGL LINESを指定したため、ペアを 12 作る動作が起きる)。 13  頂点のペアはラスタライザで線分を構成する画素点にラスタライズされる。ペア 14 は二つあるから、それぞれに対応するラスタライザが独立に並列実行する。 15 フラグメントシェーダーの動き ラスタライザから出力された各画素点に対応してそれぞ 16 れフラグメントシェーダーが独立して並列実行される。 17  フラグメントシェーダーからgl FragColorに出力された色情報はGPUのフレー 18 ムバッファでひとつにまとめられ、一枚の画像になる。 19  今回の例題では、2本の線分の交わる画素点では重複して色情報がフレームバッ 20 ファへ出力される。そのときにどの色を使うかは実行のタイミングによって様々で 21

(28)

ある。3D CGの場合には、画素点の深さ情報によって実際に描画する色を選択して 1 いる(視点から被写体までの距離の短い方の色を選択する)。 2 2.2.5 エラーに関する注意 3 エラー処理について注意が必要である。 4 ここで示したプログラムでは、基本動作の理解を優先するために、煩雑なエラー処理 5 (エラーが起きたことを検出する処理、エラー内容を表示する処理、実行を強制終了する 6 処理)を全て省いている。本来は、これを加えるべきである。さもないとシェーダーソー 7 スプログラムに構文エラーがあった場合でもそのエラーに気づかないということになりか 8 ねない。 9

(29)

✓ ✏ struct Position2D { float x; float y; }; ✒ ✑ 図2.15: 2次元座標値を保持する構造体 ✓ ✏ struct ArrayBuffer { GLuint bufID; //参照番号

int size; //個々のattribute変数のデータの個数

ArrayBuffer(float* data, int s, int n); //コンストラクタ

}; ✒ ✑ 図2.16: 入力データを保持するオブジェクトのクラス

2.3

簡単な線画:クラスの導入

1 前節で紹介したスタイルのプログラムは可読性がきわめて低い。そこでプログラムの一 2 部をオブジェクト指向の手法でクラス化し、記述を整理する。 3 2.3.1 2 次元座標構造体:Position2D 4 まず、図2.15の構造体 Position2Dは2次元座標値を保持するために使用する。 5 2.3.2 入力データ配列クラス:ArrayBuffer 6 図2.16のクラスArrayBufferは、GPUへ投入する入力データをオブジェクト化して管理 7 する。図2.8の複雑な処理をこのクラスの中に隠すことが目的である。なお、ここではデー 8 タのカプセル化は行わないため、クラスの定義にはclassキーワードは用いず、struct 9 を用いる。よってクラスのメンバーは全て外部に無条件で公開される。もし、classを用 10 いるならば、クラス宣言の冒頭は以下のようになる。 11 class ArrayBuffer { 12 public: 13 ... 以下、structの場合と同じ 14 このクラスはコンストラクタのみを含むが、その仕様は以下の通りとする。 15

(30)

✓ ✏ struct Shader {

GLuint program; //シェーダー実行可能プログラムの参照番号 Shader(const char* vsn, const char* fsn); //コンストラクタ

void use(); //使用の宣言

void bindArrayBuffer(const char* vname, ArrayBuffer* ap);

//attribute変数配列の設定

void run(GLenum mode, int n); //描画実行

GLuint compileProgram(GLenum type, const GLchar *file);

//シェーダーソースプログラムのコンパイル

void buildProgram(const GLchar *vsfile, const GLchar *fsfile);

//シェーダ実行可能プログラムの構築

};

✒ ✑

図 2.17: シェーダーオブジェクトのクラス

ArrayBuffer(float* data, int s, int n) ... 第1引数には、入力データがすでに計

1 算され、格納されているfloat型配列を指定する。第2引数には、それぞれのバー 2 テックスシェーダーへ供給するfloat型データの個数(=この入力データに対応する 3 attribute変数に含まれるfloat型データの個数)を指定する。第3引数には、バー 4 テックスシェーダーで処理する頂点の個数を指定する。 5 なお、メンバー変数 bufID、sizeは内部でのみ使用する変数である。このクラスにはコ 6 ンストラクタのみがあり、必要な全ての作業はオブジェクトを作る作業の中で行う。 7 2.3.3 シェーダークラス:Shader 8 図2.17のクラスShaderは、シェーダープログラムをオブジェクト化して管理する。こ 9 れは図2.6、図2.7の複雑な処理を隠すことを目的としている。メンバー関数の内容は以 10 下の通りである。 11

Shader(const char* vsn, const char* fsn) ... コンストラクタである。第1、第2 12 引数にバーテックスシェーダー、フラグメントシェーダーのソースファイル名を文 13 字列で指定する。 14 void use() ... このシェーダー実行可能プログラムをこれ以降、使うことを宣言する。 15

(31)

✓ ✏ Shader *sp;//シェーダー・オブジェクトのポインタ

void initSystem(int argc, char *argv[]) { glutInit(&argc,argv); glutInitDisplayMode(GLUT_RGB|GLUT_SINGLE); glutInitWindowSize(512,512); glutCreateWindow("Test Window"); glClearColor(0.5,0.5,0.5,1); #if defined(WIN32) glewInit(); #endif sp = new Shader("shader.vert","shader.frag"); //ここが簡単になった sp->use(); } ✒ ✑ 図2.18: クラスを用いた initSystem() 字列で指定する。第2引数に、そのattribute変数へデータを供給するArrayBuffer 1 オブジェクトのアドレスを指定する。 2

void run(GLenum mode, int n) ... 描画を行う関数である。第 1引数に描画モード

3 (GL LINESなど)を指定する。第2引数に頂点数を指定する。 4 なお、メンバー変数programは内部でのみ使用する変数である。メンバー関数 5 compileProgram()、buildProgram() も内部でのみ使用する関数である。 6 2.3.4 初期関数、描画関数の変更 7

上記のクラス、オブジェクトを用いると、関数initSystem()、initData()、display()

8 はそれぞれ図2.18∼図2.20の通りに簡単化される。 9 2.3.5 クラスの実装 10 ArrayBufferクラスのコンストラクタの実装は図2.21の通りである。Shaderクラスの 11 実装は図2.22の通りである。これら実装は、前節のプログラムを組み合わせたものになっ 12 ている。 13

(32)

✓ ✏ const int NUM_POINTS = 4;//頂点の数

void initData() { Position2D pos[NUM_POINTS]; pos[0].x = -0.5; pos[0].y = -0.5; pos[1].x = +0.5; pos[1].y = +0.5; pos[2].x = +0.5; pos[2].y = -0.5; pos[3].x = -0.5; pos[3].y = +0.5;

ArrayBuffer ab((float*)pos, 2, NUM_POINTS);

//入力データオブジェクトの生成 sp->bindArrayBuffer("position",&ab); //positionとabを結びつける glLineWidth(10.0); } ✒ ✑ 図2.19: クラスを用いたinitData() ✓ ✏ void display(void) { glClear(GL_COLOR_BUFFER_BIT); sp->run(GL_LINES,NUM_POINTS); } ✒ ✑ 図2.20: クラスを用いたdisplay() ✓ ✏

ArrayBuffer::ArrayBuffer(float* data, int s, int n){ size = s;

glGenBuffers(1, &bufID);

glBindBuffer(GL_ARRAY_BUFFER, bufID);

glBufferData(GL_ARRAY_BUFFER, sizeof(float)*size*n, data, GL_STATIC_DRAW);

}

✒ ✑

(33)

✓ ✏ Shader::Shader(const char* vsn, const char* fsn){

buildProgram(vsn, fsn); }

void Shader::use(){

glUseProgram(program); }

void Shader::bindArrayBuffer(const char* vname, ArrayBuffer* ap){ glBindBuffer(GL_ARRAY_BUFFER, ap->bufID);

GLint p = glGetAttribLocation(program,vname);

glVertexAttribPointer(p, ap->size, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(p);

}

void Shader::run(GLenum mode, int n){ glDrawArrays(mode,0,n);

glFlush(); }

GLuint Shader::compileProgram(GLenum type, const GLchar *file) {

FILE* fp = fopen(file, "r"); #define MAX_SHADER_FILE_SIZE 10000

GLchar ftext[MAX_SHADER_FILE_SIZE];

unsigned long n = fread(ftext, 1, MAX_SHADER_FILE_SIZE, fp); ftext[n] = ’\0’;

fclose(fp);

GLuint shader = glCreateShader(type);

const GLchar* ftext_handler = (const GLchar*)&ftext; glShaderSource(shader, 1, &ftext_handler, NULL); glCompileShader(shader);

return shader; }

void Shader::buildProgram(const GLchar *vsfile, const GLchar *fsfile){ GLuint vs = compileProgram(GL_VERTEX_SHADER, vsfile);

GLuint fs = compileProgram(GL_FRAGMENT_SHADER, fsfile); program = glCreateProgram(); glAttachShader(program, vs); glAttachShader(program, fs); glLinkProgram(program); } ✒ ✑ 図 2.22: Shaderの実装

(34)

✓ ✏ void Shader::buildProgram(const GLchar *vsfile, const GLchar *fsfile){

GLuint vs = compileProgram(GL_VERTEX_SHADER, vsfile); GLuint fs = compileProgram(GL_FRAGMENT_SHADER, fsfile); program = glCreateProgram();

glAttachShader(program, vs); glAttachShader(program, fs); glLinkProgram(program); GLint status;

glGetShaderiv(program, GL_LINK_STATUS, &status); if (!status)

{

cerr << "program link error: " << endl; GLsizei infologLength;

glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infologLength); GLchar * infoLog = new GLchar[infologLength];

if (infoLog == NULL) {

cerr << "ERROR: Could not allocate InfoLog buffer" << endl; }else{

int charsWritten = 0;

glGetProgramInfoLog(program, infologLength, &charsWritten, infoLog); cerr << "Program InfoLog:" << endl << infoLog << endl; delete infoLog; } exit(1); } } ✒ ✑ 図2.23: リンクエラーを検知する処理を追加した場合

2.4

エラー処理

1 ここで再度エラー処理について触れる。 2 もし図2.17のShader::compileProgram()にコンパイルエラーが起きたことを検知す 3 る処理、Shader::buildProgram()にリンクエラーが起きたことを検知する処理を追加す 4 るならば、それぞれのメンバー関数は図2.24、図2.23のように作らねばならない。コン 5 パイル、リンクを行なった後にエラーの有無を調べ、もしエラーがあるならば、そのメッ 6 セージをエラー出力ストリーム cerr へ出力する。詳細は述べない。 7 ホストプログラムに記載したattribute変数名がバーテックスシェーダソースプログラ 8 ム内の実際のattribute変数名と異なるエラーはありがちである。この場合には 9 Shader::bindArrayBuffer()内で求めるattribute変数の場所pが負数となるエラーが

(35)

✓ ✏ GLuint Shader::compileProgram(GLenum type, const GLchar *file)

{

FILE* fp = fopen(file, "r"); #define MAX_SHADER_FILE_SIZE 10000

GLchar ftext[MAX_SHADER_FILE_SIZE];

unsigned long n = fread(ftext, 1, MAX_SHADER_FILE_SIZE, fp); ftext[n] = ’\0’;

fclose(fp);

GLuint shader = glCreateShader(type);

const GLchar* ftext_handler = (const GLchar*)&ftext; glShaderSource(shader, 1, &ftext_handler, NULL); glCompileShader(shader);

GLint status;

glGetShaderiv(shader, GL_COMPILE_STATUS, &status); if (!status)

{

cerr << "shader compile error: " << file << endl; GLsizei infologLength;

glGetProgramiv(shader, GL_INFO_LOG_LENGTH, &infologLength); GLchar * infoLog = new GLchar[infologLength];

if (infoLog == NULL) {

cerr << "ERROR: Could not allocate InfoLog buffer" << endl; }else{

int charsWritten = 0;

glGetShaderInfoLog(shader, infologLength, &charsWritten, infoLog); cerr << "Shader InfoLog:" << endl << infoLog << endl;

delete infoLog; } exit(1); } return shader; } ✒ ✑ 図2.24: コンパイルエラーを検知する処理を追加した場合

(36)

✓ ✏ void Shader::bindArrayBuffer(const char* vname, ArrayBuffer* ap){

glBindBuffer(GL_ARRAY_BUFFER, ap->bufID); GLint p = glGetAttribLocation(program,vname); if(p < 0) {

cerr << "attribute name error: "<< vname << endl; exit(1);

}

glVertexAttribPointer(p, ap->size, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(p);

}

✒ ✑

図 2.25: Shader::bindArrayBuffer()にattribute変数名の齟齬に関するエラー検知処 理を追加した場合

(37)

図2.26: 画像例(その2、輝度値の線形補間)

2.5

簡単な線画:輝度の線形補間

1

2.2節の例ではattribute変数position だけを宣言し、そのattribute変数に頂点の位

2 置情報を格納した。この節では、それに加えて、頂点の輝度値を追加のattribute変数に 3 格納し、線分の輝度を滑らかに変化させてみる。画像の出力例は図2.26である。 4 なお、ここでいう線形補間とは、線分の両端点p1、p2の輝度値をB1、B2とするとき、 画素点 pの輝度値Bを B = |p2− p| |p2− p1| B1+ |p − p1| |p2− p1| B2 (2.1) と求めることをいう。GPUでは線形補間をハードウェアで実行するため、きわめて高速 5 な処理が可能である。 6 2.5.1 ホストプログラムの変更 7 ホストプログラムの変更は図2.27の関数initData()のみである。新たに配列briを用 8 意し、そこに0.0(最低輝度)、1.0(最大輝度)を順に格納してみた。そして、その配列につい 9

てArrayBufferオブジェクトを作り、それをシェーダーと結合している。“in brightness” 10

は、この配列に対応するバーテックスシェーダープログラムのattribute変数の名前である。

(38)

✓ ✏ void initData() { Position2D pos[NUM_POINTS]; pos[0].x = -0.5; pos[0].y = -0.5; pos[1].x = +0.5; pos[1].y = +0.5; pos[2].x = +0.5; pos[2].y = -0.5; pos[3].x = -0.5; pos[3].y = +0.5; ArrayBuffer ab((float*)pos,2,NUM_POINTS); sp->bindArrayBuffer("position",&ab); float bri[NUM_POINTS]; bri[0] = 0.0; bri[1] = 1.0; bri[2] = 0.0; bri[3] = 1.0; ArrayBuffer ab2((float*)bri,1,NUM_POINTS); sp->bindArrayBuffer("in_brightness",&ab2); glLineWidth(10.0); } ✒ ✑ 図 2.27: 輝度値の取り扱いを追加したinitData() 2.5.2 バーテックスシェーダープログラムの変更 1 図2.28が変更後のバーテックスシェーダープログラムである。 2 プログラムには輝度値を格納するattribute変数 in brightnessの宣言を追加する。さ 3 らに バーテックスシェーダープログラムで計算した輝度値をラスタライザを介してフラ 4 グメントシェーダープログラムへ受け渡す変数out brightnessを宣言する。バーテック 5 スシェーダーからの出力に対応する変数はvarying変数と呼ばれ、変数宣言の前にキー 6 ワードvaryingを付けて宣言する。 7

main関数では、この例には単に in brightness の値を out brightness に代入する

8 だけである。単なる代入であるがこれは省略できない。 9 2.5.3 フラグメントシェーダープログラムの変更 10 図2.29が変更後のフラグメントシェーダープログラムである。 11 プログラムにはバーテックスシェーダープログラムと同じvarying変数の宣言を加える。 12

(39)

✓ ✏ #version 120

attribute vec2 position;

attribute float in_brightness; varying float out_brightness; void main(void) { gl_Position = vec4(position,0.0,1.0); out_brightness = in_brightness; } ✒ ✑ 図2.28: 輝度値の取り扱いを追加したバーテックスシェーダプログラム ✓ ✏ #version 120

varying float out_brightness; void main(void) { gl_FragColor = vec4(out_brightness, 0.0, 0.0, 0.0); } ✒ ✑ 図2.29: 輝度値の取り扱いを追加したフラグメントシェーダプログラム がラスタライザを介して線形補間され、フラグメントシェーダープログラムのvarying変 1 数へ格納される。 2 バーテックスシェーダープログラムのout brightnessは線分の両端点の値であるが、 3 フラグメントシェーダープログラムのそれは線形補間された各画素の値であることを注意 4 する。 5

main関数では、線形補間されたout brightnessを赤色の輝度値としてRGBカラーを

6 作っている。 7 2.5.4 実行結果 8 図2.30にGPUの実行の様子を示す。 9 バーテックスシェーダープログラムに入力される時点で頂点に輝度値が追加されている。 10 この例では輝度値はそのままラスタライザに渡される。渡された輝度値は線分の両端点の 11 属性値と見なされ、線分の各画素について、その画素が線分内の位置に関する線形補間が 12

(40)

V.S. F.S. V.S. ラスタ ライザ F.S. F.S. F.S. F.S. F.S. V.S. F.S. V.S. ラスタ ライザ F.S. F.S. F.S. F.S. F.S. 図2.30: 輝度の線形補間の動作例 図2.31: 画像例(その3、RGBカラーの線形補間) なされる。補間された値はフラグメントシェーダープログラムに入力される。それの値を 1 元にRGBカラーを作れば、図2.26のような画像が生成される。 2

2.6

簡単な線画:

RGB

カラーの線形補間

3

前節の例ではfloat型の輝度値をattribute変数としてGPUへ渡したが、attribute変

4

数に格納できるデータのサイズは、2個、3個、4個のfloat型の並びに変更することも

5

できる。この節では、頂点のRGBカラー値をattribute変数に格納してみる。画像の出

(41)

✓ ✏ struct RGB { float r; float g; float b; }; ✒ ✑ 図2.32: RGBカラー値を保持する構造体 なお、ここでいう線形補間とは、線分の両端点p1、p2のRGBカラー値をC⃗1、C⃗2 とい う3次元ベクトル値とするとき、画素点 pのRGBカラー値 C⃗ を ⃗ C = |p2− p| |p2− p1| ⃗ C1+ |p − p1| |p2− p1| ⃗ C2 (2.2) とベクトル計算で求めることをいう。繰り返しになるが、GPUでは線形補間をハードウェ 1 アで実行するため、きわめて高速な処理が可能である。 2 2.6.1 ホストプログラムの変更 3 まず、RGBカラー値を保持する構造体を新たに図2.32のように定義しておく。 4 図2.33は、変更するinitData()である。関数本体の後半に配列 rgbを用意し、そこ 5 に 白、赤、緑、青のRGBカラーをこの順番に格納してみた。そして、その配列について 6

ArrayBufferオブジェクトを作り、それをシェーダーと結合している。“in color”は、こ

7 の配列に対応するバーテックスシェーダープログラムのattribute変数の名前である。 8 2.6.2 バーテックスシェーダープログラムの変更 9 図2.34が変更後のバーテックスシェーダープログラムである。 10 プログラムには輝度値を格納するattribute変数in colorを宣言する。さらに バーテッ 11 クスシェーダープログラムで計算したRGBカラー値をラスタライザを介してフラグメン 12

トシェーダープログラムへ受け渡すvarying変数 out color を宣言する。この辺の仕組

13 みは前節と同じであるが、ただしデータ型はvec3、すなわちfloat型が3個並ぶデータ 14 型である。このvec3型とホストプログラムのRGB型は完全に一致するデータ型であるこ 15 と、一致しなければいけないことを注意する。 16 2.6.3 フラグメントシェーダープログラムの変更 17 図2.35が変更後のフラグメントシェーダープログラムである。 18

(42)

✓ ✏ void initData() { Position2D pos[NUM_POINTS]; pos[0].x = -0.5; pos[0].y = -0.5; pos[1].x = +0.5; pos[1].y = +0.5; pos[2].x = +0.5; pos[2].y = -0.5; pos[3].x = -0.5; pos[3].y = +0.5; ArrayBuffer ab((float*)pos,2,NUM_POINTS); sp->bindArrayBuffer("position",&ab); RGB rgb[NUM_POINTS]; rgb[0].r = 1.0; rgb[0].g = 1.0; rgb[0].b = 1.0; //白 rgb[1].r = 1.0; rgb[1].g = 0.0; rgb[1].b = 0.0; //赤 rgb[2].r = 0.0; rgb[2].g = 1.0; rgb[2].b = 0.0; //緑 rgb[3].r = 0.0; rgb[3].g = 0.0; rgb[3].b = 1.0; //青 ArrayBuffer ab2((float*)rgb,3,NUM_POINTS); sp->bindArrayBuffer("in_color",&ab2); glLineWidth(10.0); } ✒ ✑ 図 2.33: RGBカラー値の取り扱いを行うinitData() プログラムにはバーテックスシェーダープログラムと同じvarying変数の宣言を加える。 1

main関数では、ラスタライザで線形補間されたout colorを画素の色として出力してい

2

る。なお、out colorはvec3型であり、gl FragColorはvec4型であるから、データを

3 整合させるために、不足分の透明度 0.0を追加する。 4 2.6.4 実行結果 5 図2.36に示すように、バーテックスシェーダープログラムに入力される時点で頂点に 6 RGBカラー値が追加されている。この例では、RGBカラー値をそのままラスタライザに 7 渡す。ラスタライザは渡されたRGBカラー値を線分の両端点の属性値と見なされ、その 8 値が線分の各画素について線形補間される。補間された値はフラグメントシェーダープロ 9 グラムに入力される。 10

図 2.9: GPU メモリへの頂点データの転送: (a) OpenGL コンテキストにバッファオブジェ クト bufID を生成する。 (b) bufID を Vertex Buffer Object とみなし、設定対象とする。
図 2.21: ArrayBuffer のコンストラクタの実装
図 2.25: Shader::bindArrayBuffer() に attribute 変数名の齟齬に関するエラー検知処 理を追加した場合
図 3.1: 画像例(その 9 、 1 次元テクスチャ利用の場合)
+7

参照

関連したドキュメント

A partir de ellas se obtienen estadísticas para distintas pruebas de comparación de modelos (logístico vs. saturado, submodelo vs. logístico), con distribución asintótica

There are explicit formulas in special cases, and theoretical results, which relate reduced sequences, balanced labelings, Stanley functions, and Schu- bert polynomials ([S1, EG,

Finally, in the Appendix, we prove the well-known fact that the category of ket coverings of a connected locally noetherian fs log scheme is a Galois category; this implies,

The pa- pers [FS] and [FO] investigated the regularity of local minimizers for vecto- rial problems without side conditions and integrands G having nonstandard growth and proved

&lt; &gt;内は、30cm角 角穴1ヶ所に必要量 セメント:2.5(5)&lt;9&gt;kg以上 砂 :4.5(9)&lt;16&gt;l以上 砂利 :6 (12)&lt;21&gt; l

The resonant tank is designed in such a way that the LLC stage is operated in, or very close to, the series resonant frequency (fs) for full load conditions and nominal bulk

Reference Figure 13 Typical Performance Graph (Duty Cycle vs. RTAIL) to choose a typical value programming resistor for output duty cycle (with a typical RSTOP value of 3.01 k W ).

Junction Temperature I CC1, SUPPLY CURRENT DEVICE DISABLED (mA).. Supply Current Device Switching vs. Overload Timer vs. Brown−Out Hysteresis vs.. Latch Pull−Down Voltage Threshold