0. はじめに

本記事は、私の所属するIAMASの体験拡張プロジェクトの勉強会での発表資料となります。前回の記事はこちらです。本記事では、表現力向上のため避けては通れないシェーダプログラミングの基礎についての説明となります。GLSLをちゃんと記述するためには、OpenGLのレンダリングプロセスを理解する必要があるので、そこから説明しつつ、最終的に具体的な実装まで触れていきます。

1. OpenGLのグラフィクスパイプラインの成り立ち

1.1. openFrameworksのデフォルトのシェーディングアルゴリズム

シェーディングとは何でしょうか。直訳すると陰影をつけることと理解できます。(が、シェーダが単なる影つけのプログラムでないことは後述します。)
openFrameworks ( ofLight, ofMaterial ) では、デフォルトではPhongシェーディングが採用されています。Phongシェーディングとは、光源の座標、物体の頂点座標とその法線から、その頂点の色を計算すると同時に、頂点同士をつなぐ面(ポリゴン: 三角形)の各ピクセルに線形補間で色決定するアルゴリズムです。

しかし、このアルゴリズムをCPUで逐次実行するとレンダリングにかなりの時間がかかってしまいます。これを高速化する方策として、各ピクセル毎の計算が同種かつ独立している(他の計算結果に依存しない)という点から、並列化させることがあげられます。このアイディアを実現するために、CPUを補完する演算装置であるGPU(Graphics Processing Unit)が発明され、普及しました。今では市販のPCやスマートフォンにはほぼ組み込まれています。

glut , glfw といったOpenGLの高機能なラッパーライブラリになるほど、シェーディングアルゴリズムは基本機能として既にGPUプログラムが実装されており、プログラマはここをプログラミングするまでもなくAPIを叩くのみで使えます。生のOpenGLはシェーディングも実装してあげる必要がありますが、oFでは隠蔽しPhongシェーディングをデフォルトとしています。

1.2. テクスチャサンプリング

同様に、Photoshopなどでつくった模様を質感として物体に適用するのも基本機能として備わっています。面に貼りたい画像を指定すれば、最終的な画面に出力される色決定に組み込まれます。テクスチャ画像の角を頂点に対応付け、頂点間のポリゴンの色に対してはテクスチャ画像の正規化座標ででピクセル色を参照します。

1.3. グラフィクスパイプライン

シェーディングは、CGを画面に描画するためのごく一部の計算でしかありません。出力したい画面サイズで、ピクセル毎の色計算を全て行うことが「レンダリング」ですが、1フレームをレンダリングするためには以下の処理を逐次実行する必要があります。

1) 空間内の物体の座標、頂点決定
2) 物体の頂点の色計算
3) 与えられた頂点から面分割 (テッセレーション)
4) レンダリングする画面のピクセル情報へと変換(ラスタライズ)
5) ピクセルの色決定
6) ブレンディング、隠面消去、深度テスト
7) 画面全体を描画情報として保持 (フレームバッファの保持)

上記のプロセスは、こちらの記事にわかりやすい図解が載っています。

1)をC++などのコードで記述することは、馴染みがあります(配列をつくって、座標計算する)が、それ以降のプロセスは毎フレーム無意識に実行されています。この隠蔽されたレンダリングのプロセスをグラフィクスパイプラインと呼びます。

2. シェーダの役割

2.1.シェーディングをプログラマブルにする

陰影をつけるアルゴリズムの固定化は、プログラマの手間が省けて便利である反面、表現の制約にもなります。たとえばPhongシェーディングは、ある種とても「CGっぽい」質感になってしまいます。もし仮にセル画風、アニメ風の質感のCGをつくりたい場合、Phongシェーディングは明らかに不向きです。このような自由度へのニーズに答えるために、ピクセルの色(陰影)計算をプログラマブルに実装できることが必要になってきます。こうしたニーズを受け、プログラマが操作できる関数群とそのコンパイラを提供しているのが、OpenGLの機能であるGLSL(OpenGL Shading Language)です。

1) 空間内の物体の座標、頂点決定
2) 物体の頂点の色計算 — [ Vertex shader ]
3) 与えられた頂点から面に分割 (テッセレーション) — [ Geomtery shader ]
4) レンダリングする画面のピクセル情報へと変換 (ラスタライズ)
5) ピクセルの色決定 — [ Fragment shader ]
6) ブレンディング、隠面消去、深度テスト
7) 画面全体を描画情報として保持 (フレームバッファの保持)

グラフィクスパイプラインはかつてはプログラマブルではない固定機能でしたが、GLSLの機能提供によって、2), 3), 5)をプログラミングできるようになりました。それぞれの部分のGLSLコードを Vertex shader, Geomtery shader, Fragment shaderとよびます。

2.2.シェーディング以外の可能性

GLSLを記述できるということは、単に陰影処理だけができるわけではありません。
頂点の位置をGPUで動的に変更したり、面のピクセル色を動的に変更したり、様々なサンプリング情報(テクスチャだけでなく、法線マップ、バンプマップなど)を加味して算出したり、リアルな表現のためのアイディア実装が考えられる他、力技でCPU処理を高速なGPU側に移植することも考えられます。
もはや、リアルタイムレンダリングにおける表現力、パフォーマンスに大きな影響をおよぼすのは、GLSLのコーディングリテラシーとなっています。

一度レンダリングされたFrame BufferをさらにFragment shaderで色を再計算するポストエフェクトは、シェーディングよりも馴染みのあるポピュラーな領域かもしれません。また、Fragment shaderのピクセル毎計算によるRay Marchingなどの Shader Art もホットなトピック(?)です。

3. 記述に必要なこと

3.1. 文法理解

他のプログラミング言語と同様、文法を覚える必要があります。数学系の型(vec4, mat4など)や関数(abs(), pow(), sqrt()など)は、比較的覚えるのがラクといえます。ただ、GLSL特有の修飾子(atribute, uniform, in, out, inout, layoutなど)は意味を押さえる必要があります。また Swizzle演算子 という、ベクトル系の型に対する独特の要素へのアクセス方法がありますので合わせて理解しておく必要があります。文法については、公式のドキュメントを参照するのが最も確実ですが、使用するGLSLのバージョンは何かは自覚的である必要があります。機能にかなりの差異があります。

3.2. ベクトルと行列

GLSLにはある機能は数学的な処理を記述する関数くらいしか無いために、乱数生成、回転や移動、レンダリング画面への座標変換を自力で記述する必要があります。その他、陰影処理を施すだけでもそれなりの計算を自力で記述する必要があります。物理・数学の法則を理解し、コードとして実装する力がGLSLプログラミングでは要求されます。

4.コード

ここでは、建築物と雪を降らせる絵を出力するプログラムを示します。

Snow in the city: GPU particle system #openFrameworks #creativecoding #generativeart #xmas

Ayumu Nagamatsuさん(@ayumu_naga)が投稿した動画 –

コードは こちら に全文を格納しました。
以下、簡単な解説になります。

main.cpp / ofApp.h / ofApp.cpp / Buildings.hpp

main.cppでは、OpenGLのバージョンを3.2にし、GLSL 150を利用できるようにしています。
Buildings.hpp は Buildings というclassを定義しています。再帰計算によって、ブロックを積み重ねるようにしてビル群を形成しています。
ofApp.cpp においてはシェーダに橋渡しをするようなプログラムを書いています。setup関数内でshaderのロード、Geometry shaderの入出力の指定、またdraw関数内でShader側に必要な変数を渡しています。

Vertex shader

雪個々の位置情報は、単純なものでもC++側で100万頂点の更新を毎フレーム繰り返すとさすがに負荷が高いです。よって、単純に下降していくような座標変換もVertex shader内で記述するようにします。

Geometry shader

Vertex shaderのみでは、雪はまだ単なる点です。これに雪のテクスチャを適用するためには、まず点を板ポリゴンにする必要があります。この処理をGeometry shader内で書いています。これはかなり単純な部類のテッセレーションと言えます。面にする際に、後続の処理で使うためのテクスチャ座標も持たせます。
また、面それぞれの回転もここでもたせます。回転は、rotateみたいな関数は無いので(辛)、回転行列をつくってそれぞれの頂点に乗算します。

Fragment shader

テクスチャ座標に対応する色情報をテクスチャ画像から取り出し、それを出力としてoutput用の変数に代入しています。