5. シェーダープレイグラウンド
此れまでのテスターでは、新しい事を試す度にCのコードを変えていました。
色を変えるにもユニフォームを追加するにもCを書き直し、シェーダーを直したらプログラムを再起動する必要がありました。
此の章では、テスターをシェーダープレイグラウンドに作り変えます。
画面全体に1つのフラグメントシェーダーを描き、時間・画面の大きさ・マウスの位置を自動で渡し、Rキーを押すと再起動せずにシェーダーを読み込み直します。
第6章から第10章、及び第14章は、全て此のプレイグラウンドのshader/playground.glslだけを書き換えて進めます。
Shadertoyの様なウェブのシェーダーエディターと同じ考え方で、「GLSLを書く→保存→R→結果を見る」を数秒で繰り返せる様にするのが目的です。
5.1 プレイグラウンドの全体像
プレイグラウンドは、第1章でダウンロードしたmain.c(第4章でも変更していない物)を土台にしています。
変更点は次の通りです。
- シェーダーのコンパイルとリンクを
compileShader()とbuildProgram()と言う関数にまとめ、失敗したら0を返す様にしました。 - Rキーでシェーダーを読み込み直します。失敗した場合は前のシェーダーを使い続けます。
- ESCキーで終了します。
- 毎フレーム、
uTime(起動からの秒数)、uResolution(画面のピクセル数)、uMouse(マウスの位置)をユニフォームで渡します。 - macOSでは、シェーダーの
#version 450を自動で#version 410に書き換えます。其の為、.mac.glslファイルはもう必要ありません。 - フラグメントシェーダーは
shader/playground.glsl、頂点シェーダーは今まで通りshader/vertex.glslです。 - 色をアニメーションさせていたCのコードは削除しました。時間は
uTimeでシェーダーに渡すので、アニメーションはシェーダーの中で書けます。
変更の全体は記事の最後のDIFFで確認出来ます。
此処では、新しい部分を順番に見ていきます。
5.2 ビルドと実行
第4章でmain-ambient.cやmain-directional.cに切り替えていた場合は、main.cに戻して下さい。
- Windows:
main.cの「ビルドから除外」を「いいえ」に戻し、main-ambient.cとmain-directional.cを「はい」にします。F5で起動します。 - macOS:
main.cの「Target Membership」にチェックを入れ直し、他の2つは外します。shader/vertex.glslとshader/playground.glslをプロジェクトに追加し、「Build Phases」→「Copy Files」に加えます(プレイグラウンドは.mac.glslではなく此の2つを読みます)。更に、「Product」→「Scheme」→「Edit Scheme」→「Run」→「Options」で「Working Directory」に「Use custom working directory」を選び、プロジェクトのフォルダを指定して下さい。そうしないと、Rキーで読み込まれるのはビルドの時にコピーされた古いファイルで、編集したplayground.glslではありません。 - Unix:引数無しで
./build-unix.shを実行すると、main.cからビルドして起動します。
起動すると、ウィンドウのタイトルが「GLSLシェーダープレイグラウンド」になり、ゆっくり色が変わる背景と、マウスに付いてくる白い円が表示されます。
5.3 C側の変更
ウィンドウの大きさを覚えておく
シェーダーに画面の大きさを渡すには、Cのプログラムが其れを知っていなければなりません。
ファイルの先頭近くに2つのグローバル変数を追加し、リサイズのコールバックで更新します:
// ウィンドウの大きさとマウスの位置(シェーダーに渡す)
static int gWidth = 800;
static int gHeight = 600;
// ビューポートのリサイズコールバック
void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
(void)window;
gWidth = width;
gHeight = height;
glViewport(0, 0, width, height);
}
起動時のビューポートも、決め打ちの800×600ではなく、実際のフレームバッファの大きさから設定します:
// ビューポートを設定(Retina等では実際のピクセル数がウィンドウの大きさと違う)
glfwGetFramebufferSize(window, &gWidth, &gHeight);
glViewport(0, 0, gWidth, gHeight);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
GLFWには2種類の「大きさ」が有ります。
- ウィンドウの大きさ(
glfwGetWindowSize):OSの画面座標の単位。glfwCreateWindow(800, 600, ...)で指定したのは此方です。 - フレームバッファの大きさ(
glfwGetFramebufferSize):実際のピクセル数。OpenGLが描くのは此方です。
普通のモニターでは2つは同じですが、MacのRetinaディスプレイ等の高DPIの画面では、800×600のウィンドウのフレームバッファが1600×1200ピクセルになります。
ビューポートを800×600のままにすると、画面の左下4分の1にしか描かれません。
gl_FragCoord(後で説明します)はフレームバッファのピクセル単位なので、シェーダーに渡すのもフレームバッファの大きさです。
compileShader と buildProgram
今まではmainの中でシェーダーをコンパイルしていましたが、Rキーで読み込み直す為に、同じ処理を何度でも呼べる関数にします:
// シェーダーをコンパイルし、成功したかどうかを返す
GLuint compileShader(GLenum type, const char *source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
checkShaderCompile(shader);
glDeleteShader(shader);
return 0;
}
return shader;
}
compileShaderは、成功したらシェーダーオブジェクトを、失敗したらエラーログを表示してから0を返します。
OpenGLのオブジェクト名に0が使われる事は無いので、0を「失敗」の印に使えます。
失敗したシェーダーはglDeleteShaderで削除しておかないと、Rを押す度にリークします。
// 頂点シェーダーとフラグメントシェーダーのファイルからプログラムを作る(失敗したら0)
GLuint buildProgram(const char *vertexPath, const char *fragmentPath) {
char *vertexSource = readShaderFile(vertexPath);
char *fragmentSource = readShaderFile(fragmentPath);
if (!vertexSource || !fragmentSource) {
free(vertexSource);
free(fragmentSource);
return 0;
}
#ifdef __APPLE__
// macOSはOpenGL 4.1までなので、「#version 450」を「#version 410」に書き換える
char *v;
if ((v = strstr(vertexSource, "#version 450"))) v[10] = '1';
if ((v = strstr(fragmentSource, "#version 450"))) v[10] = '1';
#endif
GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexSource);
GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentSource);
free(vertexSource);
free(fragmentSource);
if (!vertexShader || !fragmentShader) {
if (vertexShader) glDeleteShader(vertexShader);
if (fragmentShader) glDeleteShader(fragmentShader);
return 0;
}
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
GLint success;
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
checkProgramLink(program);
glDeleteProgram(program);
return 0;
}
return program;
}
buildProgramは、ファイルの読み込み、コンパイル、リンクの何処で失敗しても、確保した物を全て解放してから0を返します。
free(NULL)は何もしない事がC言語で決まっているので、片方だけ読み込めた場合でもfreeを2回呼んで安全です。
strstrを使う為に、ファイルの先頭に#include <string.h>を追加しています。
macOSの#versionの書き換え
macOSのOpenGLは4.1までしか無いので、#version 450のシェーダーはコンパイル出来ません。
第1章から第4章では其の為に.mac.glslと言う別のファイルを用意していましたが、プレイグラウンドでは同じファイルを全てのOSで使える様に、読み込んだ文字列を書き換えます。
strstr(s, "#version 450")は、文字列sの中で"#version 450"が最初に現れる位置へのポインターを返します(見つからなければNULL)。
其処から数えると:
| 添字 | 0 | 1〜7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|
| 文字 | # | version | 空白 | 4 | 5 | 0 |
10番目の'5'を'1'に変えると#version 410になります。
ファイルを書き換えるのではなく、メモリの中のコピーだけを変えているので、WindowsやUnixでは元の#version 450のまま使われます。
但し、此れは版数の数字を変えるだけです。
GLSL 4.50にしか無い機能(例えばコンピュートシェーダーや一部の組み込み関数)を使うと、Macではコンパイルエラーになります。
此のシリーズのプレイグラウンドの章では、4.10でも動く機能だけを使います。
又、#version 460の様に別の版数を書くと書き換えられないので、プレイグラウンドのシェーダーは必ず#version 450 coreで始めて下さい。
シェーダーを読み込む
mainの中では、buildProgramを1回呼ぶだけになりました:
// シェーダーを読み込む
const char *vertexPath = "shader/vertex.glsl";
const char *fragmentPath = "shader/playground.glsl";
GLuint shaderProgram = buildProgram(vertexPath, fragmentPath);
if (!shaderProgram) {
puts("シェーダーの読み込みに失敗しました");
glfwDestroyWindow(window);
glfwTerminate();
return -1;
}
起動時に失敗した場合は、使えるシェーダーが1つも無いので終了します。
頂点データ(フルスクリーンクアッド)とVAOの設定は第1章のmain.cと同じです。
ESCとRキー
メインループの最初でキーボードを調べます:
int reloadWasDown = 0;
// メインレンダリングループ
while (!glfwWindowShouldClose(window)) {
// ESCキーで終了
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1);
// Rキーでシェーダーを再読み込み(失敗したら古いシェーダーのまま)
int reloadDown = glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS;
if (reloadDown && !reloadWasDown) {
GLuint newProgram = buildProgram(vertexPath, fragmentPath);
if (newProgram) {
glDeleteProgram(shaderProgram);
shaderProgram = newProgram;
puts("シェーダーを再読み込みしました");
} else {
puts("再読み込みに失敗しました。前のシェーダーを使い続けます");
}
}
reloadWasDown = reloadDown;
glfwGetKeyは「今キーが押されているか」を返します。
人がRキーを押してから離すまでには、60fpsなら数フレームから十数フレームかかります。
単にif (reloadDown)と書くと、其の間ずっと毎フレームシェーダーを読み込み直してしまいます。
其処で、前のフレームの状態reloadWasDownを覚えておき、「前のフレームでは押されていなくて、今のフレームで押されている」瞬間(押された瞬間)だけ読み込み直します。
此れをエッジ検出と言います。
新しいプログラムの作成に失敗した場合、古いshaderProgramは削除しません。
何故でしょうか?
もし古いプログラムを先に削除してしまうと、描画に使えるプログラムが無くなり、画面が黒くなるか、エラーになります。
シェーダーを書いている時は、セミコロンを1つ忘れる様な失敗は日常茶飯事です。
古いシェーダーのまま動き続ければ、ターミナルのエラーを見ながら直して、もう一度Rを押すだけで済みます。
新しいプログラムが出来た時だけ、古い物を削除して入れ替えます。
例えば、out_color = vec4(color, 1.0);のセミコロンを消してRを押すと、ターミナルには次の様に表示され、画面は前の状態のまま動き続けます:
シェーダーファイルを読み込み中: shader/vertex.glsl
シェーダーファイルを読み込み中: shader/playground.glsl
シェーダーのコンパイルに失敗しました:
0:22(1): error: syntax error, unexpected '}', expecting ',' or ';'
再読み込みに失敗しました。前のシェーダーを使い続けます
0:22(1)は「0番目のソース文字列の22行目、1文字目」と言う意味です。
セミコロンが無い事に気付くのは次の行の}を読んだ時なので、エラーの行番号は実際の間違いの1行後になっています。
エラーメッセージの書式はGPUのドライバーによって違います(此れはMesaの書式で、NVIDIAでは0(22) : error C0000: ...の様になります)。
ユニフォームを渡す
毎フレーム、描画の前に3つのユニフォームを設定します:
// マウスの位置(ピクセル、左下が原点になる様にyを反転)
double mouseX, mouseY;
glfwGetCursorPos(window, &mouseX, &mouseY);
int windowW, windowH;
glfwGetWindowSize(window, &windowW, &windowH);
float scale = windowW > 0 ? (float)gWidth / (float)windowW : 1.0f;
// ユニフォームを設定
glUniform1f(glGetUniformLocation(shaderProgram, "uTime"), (float)glfwGetTime());
glUniform2f(glGetUniformLocation(shaderProgram, "uResolution"), (float)gWidth, (float)gHeight);
glUniform2f(glGetUniformLocation(shaderProgram, "uMouse"),
(float)mouseX * scale, (float)(windowH - mouseY) * scale);
uTime:glfwGetTime()、GLFWを初期化してからの秒数です。uResolution:フレームバッファの幅と高さ(ピクセル)です。uMouse:マウスの位置をフレームバッファのピクセルに変換した物です。
glfwGetCursorPosは、ウィンドウの左上を原点とし、yが下向きの、ウィンドウの単位の座標を返します。
一方、OpenGLのgl_FragCoordは左下が原点で、yが上向きの、フレームバッファのピクセルです。
其処で2つの変換をします。
- yを反転します:。
- ウィンドウの単位からピクセルに変換します:を掛けます。
数字で確かめましょう。
800×600の普通のウィンドウで、カーソルがに有る場合、なので:
同じウィンドウがRetinaディスプレイに有り、フレームバッファが1600×1200なら、なので:
どちらの場合も、シェーダーから見ると「画面の右から30%、上から3分の1」の同じ場所です。
windowW > 0の確認は、ウィンドウが最小化されて大きさが0になった時の0除算を防ぐ為です。
シェーダーが或るユニフォームを使っていない場合、GLSLコンパイラーは其れを削除するので、glGetUniformLocationは−1を返し、其のglUniform*は無視されます(第3章)。
其の為、プレイグラウンドのシェーダーはuTime、uResolution、uMouseの中で必要な物だけを宣言すれば良いのです。
glGetUniformLocationを毎フレーム呼ぶのは少し無駄ですが、Rで読み込み直すとプログラムが変わってロケーションも変わり得るので、此処では分かり易さを優先しています。実際のエンジンでは、プログラムを作った時にロケーションを調べて保存しておきます。
5.4 GLSL側:playground.glsl
プレイグラウンドの最初のシェーダーは次の通りです:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform float uTime; // 起動してからの秒数
uniform vec2 uResolution; // 画面の大きさ(ピクセル)
uniform vec2 uMouse; // マウスの位置(ピクセル、左下が原点)
void main() {
// ピクセル座標を0〜1に正規化
vec2 uv = gl_FragCoord.xy / uResolution;
// 時間で動くグラデーション
vec3 color = 0.5 + 0.5 * cos(uTime + uv.xyx + vec3(0.0, 2.0, 4.0));
// マウスの周りに白い円
float d = length(gl_FragCoord.xy - uMouse);
color = mix(color, vec3(1.0), 1.0 - smoothstep(18.0, 20.0, d));
out_color = vec4(color, 1.0);
}
gl_FragCoord
gl_FragCoordはフラグメントシェーダーの組み込みの入力変数(vec4)で、xyは其のフラグメントのフレームバッファの中でのピクセル座標です。
左下のピクセルの中心が、右上のピクセルの中心がです。
(zは深度、wはですが、2Dのプレイグラウンドでは使いません。)
第3章では頂点シェーダーから貰うvUvで「画面の何処か」を知りましたが、gl_FragCoordならデータを何も渡さなくても分かります。
フルスクリーンクアッドでは、gl_FragCoord.xy / uResolutionはvUvと同じ値になります。
時間で動くグラデーション
0.5 + 0.5 * cos(...)は、第3章のCのアニメーションと同じ「−1〜1を0〜1に変換する」形です:
uv.xyxはスウィズル(第2章)で、のvec3を作ります。
cosにvec3を渡すと、3つの成分其々のコサインを計算します。
R、G、Bに0、2、4ラジアンのずれが有るので、第3章の120°(約2.09ラジアン)のずれと同じ様に、3つのチャンネルの波がずれて色相が回ります。
で左下()を計算すると:
オレンジがかった赤です。
第3章ではCで9個のサインを計算しましたが、此処ではシェーダーの1行で、しかもピクセル毎に違う色になります。
マウスの円
length(gl_FragCoord.xy - uMouse)は、此のピクセルからマウスまでの距離(ピクセル)です。
smoothstep(18.0, 20.0, d)は、距離が18以下で0、20以上で1、其の間で滑らかに変わります。
1.0 - で反転すると「円の中で1、外で0」になり、其れをmixの係数にして白を混ぜます。
18〜20の2ピクセルの幅で滑らかに変わるので、円の縁がギザギザになりません(アンチエイリアス。第6章で詳しく扱います)。
5.5 座標の正規化と縦横比
プレイグラウンドで図形を描く時は、殆ど毎回、最初にピクセル座標を扱い易い座標に変換します。
3つの段階を見ていきましょう。
0〜1のuv
gl_FragCoord.xy / uResolutionは、vec2同士の割り算なので成分毎に割ります。
xは幅で、yは高さで割られ、どちらも0〜1になります。
座標を色として表示すると、正しく計算出来ているか一目で分かります:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform vec2 uResolution;
void main() {
vec2 uv = gl_FragCoord.xy / uResolution;
out_color = vec4(uv, 0.0, 1.0); // x → 赤、y → 緑
}
左下が黒、右下が赤、左上が緑、右上が黄色です。
もし上下が逆なら、何処かでyを反転させ過ぎています。
中心を原点にして、縦横比を直す
uvは便利ですが、図形を描くには2つの問題が有ります。
- 原点が左下の角に有ります。円等は画面の中心を原点にした方が書き易いです。
- xとyの単位が違います。800×600の画面では、uvの0.1は横80ピクセル、縦60ピクセルです。
試しに、uvを−1〜1に変換し、半径0.5の円を描いてみます:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform vec2 uResolution;
void main() {
vec2 uv = gl_FragCoord.xy / uResolution;
vec2 p = uv * 2.0 - 1.0; // −1〜1(但し横と縦で単位が違う)
float d = length(p);
vec3 color = vec3(0.1, 0.1, 0.15);
if (d < 0.5) color = vec3(1.0, 0.7, 0.2);
out_color = vec4(color, 1.0);
}
結果は横に伸びた楕円になります(下の画像の左)。
此れを直すのが、プレイグラウンドで最もよく使う1行です:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform vec2 uResolution;
void main() {
// 中心が(0, 0)、縦が−1〜1、横は画面の比率に合わせる
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
float d = length(p);
vec3 color = vec3(0.1, 0.1, 0.15);
if (d < 0.5) color = vec3(1.0, 0.7, 0.2);
out_color = vec4(color, 1.0);
}
分解すると:
2.0 * gl_FragCoord.xy - uResolution:xはからに、yはからになります。画面の中心がです。/ uResolution.y:xもyも同じ数(高さ)で割ります。yはに、xは、つまり約になります。
xとyを同じ数で割るので、1単位は縦も横も同じ300ピクセルになり、円は円のままです。
数字で確かめましょう。
左下のピクセルの中心では:
ピクセルでは:
此の座標では、ウィンドウの大きさを変えても円は常に画面の高さの4分の1の半径(直径は高さの半分)を保ちます。
第3章で800.0 / 600.0と数字を直接書いた問題が、uResolutionのお陰で解決しました。
5.6 時間を使う
uTimeが有れば、シェーダーの中だけでアニメーションが作れます。
よく使う関数は次の3つです。
sin(x)、cos(x):−1〜1の間を、周期で滑らかに往復します。sin(uTime * k)の周期は秒です。fract(x):小数部分、。0〜1を繰り返す「のこぎり波」になります。例えば、です。step(edge, x):なら0、そうでなければ1。
此れらを使って、流れる縞模様と、鼓動する円を描きます:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform float uTime;
uniform vec2 uResolution;
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
// 背景:右から左へ流れる縞模様
float stripe = step(0.5, fract(p.x * 4.0 + uTime));
vec3 color = mix(vec3(0.10, 0.10, 0.15), vec3(0.15, 0.15, 0.25), stripe);
// 鼓動する円:半径は0.3を中心に±0.1で揺れる
float radius = 0.3 + 0.1 * sin(uTime * 3.0);
float d = length(p);
float circle = 1.0 - smoothstep(radius - 0.01, radius + 0.01, d);
color = mix(color, vec3(1.0, 0.4, 0.3), circle);
out_color = vec4(color, 1.0);
}
fract(p.x * 4.0 + uTime)は、xが0.25進む毎に0〜1を1回繰り返します。step(0.5, ...)で其の前半を0、後半を1にすると、幅0.125の縞が交互に並びます。- 時間を足しているので、同じ値の場所は、つまり毎秒0.25単位ずつ左に動きます。
- 半径はで、0.2〜0.4の間を周期秒で揺れます。
uTimeはfloatなので、何時間も起動したままにすると精度が落ちます。32ビットのfloatの有効桁数は約7桁なので、約8200秒(2時間強)を超えると1ミリ秒より細かい違いを表せなくなり、10万秒(約28時間)では約8ミリ秒刻み(60fpsの1フレームの半分程)になって、アニメーションがカクカクし始めます。ゲームでは、時間を一定の周期で巻き戻したり、fract等をCのdoubleで計算してから渡したりします。
5.7 マウスに付いてくる
uMouseはピクセル単位なので、pと同じ変換をすれば同じ座標系で比べられます。
マウスを中心にした「懐中電灯」を描いてみましょう:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform vec2 uResolution;
uniform vec2 uMouse;
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec2 m = (2.0 * uMouse - uResolution) / uResolution.y; // マウスも同じ座標系に
// マウスに近い程明るい「懐中電灯」
float d = length(p - m);
float light = 1.0 - smoothstep(0.0, 0.8, d);
vec3 color = vec3(0.9, 0.8, 0.6) * light + vec3(0.03, 0.03, 0.06);
// マウスの位置に小さな点
color = mix(color, vec3(1.0, 0.2, 0.2), 1.0 - smoothstep(0.02, 0.025, d));
out_color = vec4(color, 1.0);
}
p - mは、此のピクセルからマウスへのベクトルで、其の長さが距離です(線型代数シリーズの第3章の2点間の距離)。
距離0でlight = 1、0.8以上で0になり、其の間はsmoothstepで滑らかに暗くなります。
最後に、距離0.02〜0.025の所で赤い点を描いて、マウスの位置を示します。
スクリーンショットはカーソルが(ウィンドウ座標)の時の物です。
uMouseはなので、、画面の中心より右上です。
5.8 値を色にしてデバッグする
シェーダーにはprintfが有りません。
48万個のピクセルで同時に動いているので、1つの値を表示する方法が無いのです。
其の代わり、調べたい値を色にして出力します。
此れはシェーダープログラマーが毎日使う一番大事なデバッグ方法です。
コツは次の通りです。
- 0〜1の値は其のまま
vec3(v)(灰色)や1つのチャンネルにします。 - 負になり得る値は、正を緑、負を赤の様に分けます。
- −1〜1の値(法線等)は
v * 0.5 + 0.5で0〜1にしてから表示します。 - 範囲外の値は、目立つ色(青やマゼンタ)で強調します。
- 目盛りの線を引くと、値の大きさが読めます。
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform vec2 uResolution;
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
// 調べたい値(此処ではp.x)を色にする:負は赤、正は緑
float v = p.x;
vec3 color = vec3(max(-v, 0.0), max(v, 0.0), 0.0);
// 0.25毎に細い線を引いて、値の目盛りにする
vec2 g = abs(fract(p * 4.0 + 0.5) - 0.5);
if (min(g.x, g.y) < 0.02) color += vec3(0.25);
// 1を超えた所を青で強調する(範囲外の値を見付ける)
if (abs(v) > 1.0) color.b = 1.0;
out_color = vec4(color, 1.0);
}
画面の中央で黒()、左に行く程赤、右に行く程緑になり、の両端は青が加わってマゼンタとシアンになっています。
格子の線は0.25毎なので、中央から端まで数えると、左右の端が約1.33である事が読み取れます。
目盛りの線の式abs(fract(p * 4.0 + 0.5) - 0.5)は、「一番近い0.25の倍数までの距離(0.25を1とした単位)」です。
fract(x + 0.5) - 0.5は−0.5〜0.5になり、整数の所で0になるので、其の絶対値が小さい所が線になります。
他にも、次の様な時に色のデバッグが役立ちます。
- 法線が正しいか:
out_color = vec4(normal * 0.5 + 0.5, 1.0);。上を向いた面が黄緑っぽく、右を向いた面がピンクっぽくなります。 - 値がNaN(非数)になっていないか:
if (isnan(v)) color = vec3(1.0, 0.0, 1.0);。normalize(vec3(0.0))やsqrt(-1.0)等で発生し、周りに広がって黒や変な色の点になります。 - 処理がどの分岐を通ったか:分岐毎に違う色を出力します。
5.9 此の章で使ったGLSLの機能
gl_FragCoord:フラグメントのフレームバッファでのピクセル座標(vec4)。左下のピクセルの中心が。fract(x):。0〜1を繰り返します。floor(x):以下の最大の整数。step(edge, x):なら0、そうでなければ1。sin(x)、cos(x):ラジアンの三角関数。ベクトルには成分毎に働きます。smoothstep(e0, e1, x):第3章の滑らかな0〜1の切り替え。length(v):ベクトルの長さ。length(a - b)は2点間の距離です。min(a, b)、max(a, b)、abs(x):最小、最大、絶対値。isnan(x):NaNならtrue。- スウィズル
uv.xyx:成分を並べ替えたり繰り返したりした新しいベクトル。 vec2 / vec2、vec2 / float:成分毎の割り算、全成分を同じ数で割る割り算。
5.10 ゲームやエンジンでの使い方
- Shadertoy:ウェブで最も有名なシェーダーの遊び場で、
iTime、iResolution、iMouseと言うユニフォームが有ります。此の章のプレイグラウンドと全く同じ考え方なので、Shadertoyのシェーダーは名前を変えるだけで大体動きます(但し、ShadertoyのiMouseはクリック位置等の情報も持つvec4です)。 - ポストプロセス:ゲームのブルーム、色調補正、被写界深度、ビネット等の画面効果は、全てフルスクリーンの四角形(又は三角形)に描くフラグメントシェーダーです。此の章のプレイグラウンドは其の練習になります。
- ホットリロード:UnityやUnreal Engine、Godotは、シェーダーを保存すると自動で読み込み直し、エラーが有れば前のシェーダーを使い続けます。此の章のRキーと同じ仕組みです。自作エンジンでも、ファイルの更新時刻を監視して自動で読み込み直す機能はとても役に立ちます。
- デバッグ表示:殆どのエンジンには、法線、UV、深度、ライティングだけ等を色で表示する「ビューモード」が有ります。RenderDocの様なGPUデバッガーでも、此の章の様に値を色で調べます。
- 高DPI:ウィンドウの大きさとフレームバッファの大きさの違いは、UIがぼやける、クリック位置がずれる、と言うゲームでよくあるバグの原因です。マウスの座標は必ずフレームバッファの単位に変換しましょう。
5.11 まとめ
- プレイグラウンドは、画面全体に
shader/playground.glslを描き、uTime、uResolution、uMouseを毎フレーム渡します。 buildProgram()は失敗すると0を返します。Rキーで読み込み直し、失敗したら前のプログラムを使い続けます。- キーが押された瞬間だけ反応するには、前のフレームの状態を覚えておきます(エッジ検出)。
- フレームバッファの大きさ(ピクセル)とウィンドウの大きさは、高DPIの画面では違います。シェーダーに渡すのはフレームバッファの大きさです。
- マウスの座標はyを反転し、フレームバッファのピクセルに変換します。
- macOSでは
#version 450を410に書き換えるので、同じシェーダーファイルが全てのOSで使えます。 gl_FragCoord.xy / uResolutionで0〜1、(2.0 * gl_FragCoord.xy - uResolution) / uResolution.yで中心が原点・縦横比が正しい座標になります。sin、cos、fractとuTimeでアニメーションが作れます。- シェーダーのデバッグは、値を色にして出力するのが基本です。
5.12 課題
- ★ 1280×720のウィンドウで、ピクセルの
uvとpを計算して下さい。又、pのxの範囲(左端と右端)は幾つですか? - ★
fract(3.2)、fract(-1.75)、step(0.5, 0.3)、step(0.5, 0.5)を計算して下さい。 - ★★ プレイグラウンドで、画面の中心に半径0.25の白い円を描き、
uTimeを使って半径0.5の円周上を5秒で1周させて下さい(ヒント:中心をvec2(cos(a), sin(a)) * 0.5にし、)。円は楕円にならずに回る筈です。 - ★★ マウスの位置で画面を4つに分け、左下を赤、右下を緑、左上を青、右上を白で塗って下さい(ヒント:
step(uMouse, gl_FragCoord.xy)はvec2を返します)。マウスを動かすと境界の十字が付いてくる筈です。 - ★★ わざと間違い(例えば
vec3 color = 1.0;)を書いてRを押し、ターミナルのエラーメッセージを読んで下さい。画面が前のシェーダーのまま動き続ける事を確認してから、直してもう一度Rを押して下さい。 - ★★
length(p)を色のデバッグで表示して下さい:0〜1を灰色の濃さにし、1を超えた所を赤くします。中心が黒く、半径1の円の外側が赤い画面になる筈です。 - ★★★
main.cを変更して、Rキーを押さなくてもplayground.glslが保存されたら自動で読み込み直す様にして下さい(ヒント:POSIXではstat()のst_mtime、Windowsでは_stat()でファイルの更新時刻を調べ、毎フレームではなく0.5秒毎に確認します)。保存した瞬間にエディターが書きかけのファイルを読まない様にするにはどうすれば良いかも考えて下さい。
ファイル: a/main.c | b/main.c
| 前 | 新 | ||
|---|---|---|---|
| @@ -9,6 +9,7 @@ | |||
| 9 | #include <GLFW/glfw3.h> | 9 | #include <GLFW/glfw3.h> |
| 10 | #include <stdio.h> | 10 | #include <stdio.h> |
| 11 | #include <stdlib.h> | 11 | #include <stdlib.h> |
| 12 | #include <string.h> | ||
| 12 | #include <vector.h> | 13 | #include <vector.h> |
| 13 | #include <math.h> | 14 | #include <math.h> |
| 14 | 15 | ||
| @@ -103,12 +104,78 @@ void checkProgramLink(GLuint program) { | |||
| 103 | } | 104 | } |
| 104 | } | 105 | } |
| 105 | 106 | ||
| 107 | // ウィンドウの大きさとマウスの位置(シェーダーに渡す) | ||
| 108 | static int gWidth = 800; | ||
| 109 | static int gHeight = 600; | ||
| 110 | |||
| 106 | // ビューポートのリサイズコールバック | 111 | // ビューポートのリサイズコールバック |
| 107 | void framebuffer_size_callback(GLFWwindow *window, int width, int height) { | 112 | void framebuffer_size_callback(GLFWwindow *window, int width, int height) { |
| 108 | (void)window; | 113 | (void)window; |
| 114 | gWidth = width; | ||
| 115 | gHeight = height; | ||
| 109 | glViewport(0, 0, width, height); | 116 | glViewport(0, 0, width, height); |
| 110 | } | 117 | } |
| 111 | 118 | ||
| 119 | // シェーダーをコンパイルし、成功したかどうかを返す | ||
| 120 | GLuint compileShader(GLenum type, const char *source) { | ||
| 121 | GLuint shader = glCreateShader(type); | ||
| 122 | glShaderSource(shader, 1, &source, NULL); | ||
| 123 | glCompileShader(shader); | ||
| 124 | |||
| 125 | GLint success; | ||
| 126 | glGetShaderiv(shader, GL_COMPILE_STATUS, &success); | ||
| 127 | if (!success) { | ||
| 128 | checkShaderCompile(shader); | ||
| 129 | glDeleteShader(shader); | ||
| 130 | return 0; | ||
| 131 | } | ||
| 132 | return shader; | ||
| 133 | } | ||
| 134 | |||
| 135 | // 頂点シェーダーとフラグメントシェーダーのファイルからプログラムを作る(失敗したら0) | ||
| 136 | GLuint buildProgram(const char *vertexPath, const char *fragmentPath) { | ||
| 137 | char *vertexSource = readShaderFile(vertexPath); | ||
| 138 | char *fragmentSource = readShaderFile(fragmentPath); | ||
| 139 | if (!vertexSource || !fragmentSource) { | ||
| 140 | free(vertexSource); | ||
| 141 | free(fragmentSource); | ||
| 142 | return 0; | ||
| 143 | } | ||
| 144 | |||
| 145 | #ifdef __APPLE__ | ||
| 146 | // macOSはOpenGL 4.1までなので、「#version 450」を「#version 410」に書き換える | ||
| 147 | char *v; | ||
| 148 | if ((v = strstr(vertexSource, "#version 450"))) v[10] = '1'; | ||
| 149 | if ((v = strstr(fragmentSource, "#version 450"))) v[10] = '1'; | ||
| 150 | #endif | ||
| 151 | |||
| 152 | GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexSource); | ||
| 153 | GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentSource); | ||
| 154 | free(vertexSource); | ||
| 155 | free(fragmentSource); | ||
| 156 | if (!vertexShader || !fragmentShader) { | ||
| 157 | if (vertexShader) glDeleteShader(vertexShader); | ||
| 158 | if (fragmentShader) glDeleteShader(fragmentShader); | ||
| 159 | return 0; | ||
| 160 | } | ||
| 161 | |||
| 162 | GLuint program = glCreateProgram(); | ||
| 163 | glAttachShader(program, vertexShader); | ||
| 164 | glAttachShader(program, fragmentShader); | ||
| 165 | glLinkProgram(program); | ||
| 166 | glDeleteShader(vertexShader); | ||
| 167 | glDeleteShader(fragmentShader); | ||
| 168 | |||
| 169 | GLint success; | ||
| 170 | glGetProgramiv(program, GL_LINK_STATUS, &success); | ||
| 171 | if (!success) { | ||
| 172 | checkProgramLink(program); | ||
| 173 | glDeleteProgram(program); | ||
| 174 | return 0; | ||
| 175 | } | ||
| 176 | return program; | ||
| 177 | } | ||
| 178 | |||
| 112 | int main(int argc, char **argv) { | 179 | int main(int argc, char **argv) { |
| 113 | (void)argc; | 180 | (void)argc; |
| 114 | (void)argv; | 181 | (void)argv; |
| @@ -135,7 +202,7 @@ int main(int argc, char **argv) { | |||
| 135 | #endif | 202 | #endif |
| 136 | 203 | ||
| 137 | // ウィンドウを作成 | 204 | // ウィンドウを作成 |
| 138 | GLFWwindow *window = glfwCreateWindow(800, 600, "OpenGLシェーダーテスト", NULL, NULL); | 205 | GLFWwindow *window = glfwCreateWindow(800, 600, "GLSLシェーダープレイグラウンド", NULL, NULL); |
| 139 | if (!window) { | 206 | if (!window) { |
| 140 | puts("GLFWウィンドウの作成に失敗しました"); | 207 | puts("GLFWウィンドウの作成に失敗しました"); |
| 141 | glfwTerminate(); | 208 | glfwTerminate(); |
| @@ -151,50 +218,22 @@ int main(int argc, char **argv) { | |||
| 151 | return -1; | 218 | return -1; |
| 152 | } | 219 | } |
| 153 | 220 | ||
| 154 | // ビューポートを設定 | 221 | // ビューポートを設定(Retina等では実際のピクセル数がウィンドウの大きさと違う) |
| 155 | glViewport(0, 0, 800, 600); | 222 | glfwGetFramebufferSize(window, &gWidth, &gHeight); |
| 223 | glViewport(0, 0, gWidth, gHeight); | ||
| 156 | glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); | 224 | glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); |
| 157 | 225 | ||
| 158 | // シェーダーファイルを読み込む | 226 | // シェーダーを読み込む |
| 159 | #ifdef __APPLE__ | 227 | const char *vertexPath = "shader/vertex.glsl"; |
| 160 | char *vertexShaderSource = readShaderFile("shader/vertex.mac.glsl"); | 228 | const char *fragmentPath = "shader/playground.glsl"; |
| 161 | char *fragmentShaderSource = readShaderFile("shader/fragment.mac.glsl"); | 229 | GLuint shaderProgram = buildProgram(vertexPath, fragmentPath); |
| 162 | #else | 230 | if (!shaderProgram) { |
| 163 | char *vertexShaderSource = readShaderFile("shader/vertex.glsl"); | 231 | puts("シェーダーの読み込みに失敗しました"); |
| 164 | char *fragmentShaderSource = readShaderFile("shader/fragment.glsl"); | ||
| 165 | #endif | ||
| 166 | if (!vertexShaderSource || !fragmentShaderSource) { | ||
| 167 | puts("シェーダーファイルの読み込みに失敗しました"); | ||
| 168 | if (vertexShaderSource) free(vertexShaderSource); | ||
| 169 | if (fragmentShaderSource) free(fragmentShaderSource); | ||
| 170 | glfwDestroyWindow(window); | 232 | glfwDestroyWindow(window); |
| 171 | glfwTerminate(); | 233 | glfwTerminate(); |
| 172 | return -1; | 234 | return -1; |
| 173 | } | 235 | } |
| 174 | 236 | ||
| 175 | // シェーダーをコンパイル | ||
| 176 | GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); | ||
| 177 | glShaderSource(vertexShader, 1, (const char **)&vertexShaderSource, NULL); | ||
| 178 | glCompileShader(vertexShader); | ||
| 179 | checkShaderCompile(vertexShader); | ||
| 180 | |||
| 181 | GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); | ||
| 182 | glShaderSource(fragmentShader, 1, (const char **)&fragmentShaderSource, NULL); | ||
| 183 | glCompileShader(fragmentShader); | ||
| 184 | checkShaderCompile(fragmentShader); | ||
| 185 | |||
| 186 | // シェーダープログラムをリンク | ||
| 187 | GLuint shaderProgram = glCreateProgram(); | ||
| 188 | glAttachShader(shaderProgram, vertexShader); | ||
| 189 | glAttachShader(shaderProgram, fragmentShader); | ||
| 190 | glLinkProgram(shaderProgram); | ||
| 191 | checkProgramLink(shaderProgram); | ||
| 192 | |||
| 193 | glDeleteShader(vertexShader); | ||
| 194 | glDeleteShader(fragmentShader); | ||
| 195 | free(vertexShaderSource); | ||
| 196 | free(fragmentShaderSource); | ||
| 197 | |||
| 198 | // 頂点データ(フルスクリーンクアッド)を設定 | 237 | // 頂点データ(フルスクリーンクアッド)を設定 |
| 199 | float vertices[] = { | 238 | float vertices[] = { |
| 200 | -1.0f, -1.0f, 0.0f, 0.0f, // 左下(位置、UV) | 239 | -1.0f, -1.0f, 0.0f, 0.0f, // 左下(位置、UV) |
| @@ -222,8 +261,27 @@ int main(int argc, char **argv) { | |||
| 222 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)offset); | 261 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)offset); |
| 223 | glEnableVertexAttribArray(1); | 262 | glEnableVertexAttribArray(1); |
| 224 | 263 | ||
| 264 | int reloadWasDown = 0; | ||
| 265 | |||
| 225 | // メインレンダリングループ | 266 | // メインレンダリングループ |
| 226 | while (!glfwWindowShouldClose(window)) { | 267 | while (!glfwWindowShouldClose(window)) { |
| 268 | // ESCキーで終了 | ||
| 269 | if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); | ||
| 270 | |||
| 271 | // Rキーでシェーダーを再読み込み(失敗したら古いシェーダーのまま) | ||
| 272 | int reloadDown = glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS; | ||
| 273 | if (reloadDown && !reloadWasDown) { | ||
| 274 | GLuint newProgram = buildProgram(vertexPath, fragmentPath); | ||
| 275 | if (newProgram) { | ||
| 276 | glDeleteProgram(shaderProgram); | ||
| 277 | shaderProgram = newProgram; | ||
| 278 | puts("シェーダーを再読み込みしました"); | ||
| 279 | } else { | ||
| 280 | puts("再読み込みに失敗しました。前のシェーダーを使い続けます"); | ||
| 281 | } | ||
| 282 | } | ||
| 283 | reloadWasDown = reloadDown; | ||
| 284 | |||
| 227 | // 画面をクリア | 285 | // 画面をクリア |
| 228 | glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | 286 | glClearColor(0.0f, 0.0f, 0.0f, 1.0f); |
| 229 | glClear(GL_COLOR_BUFFER_BIT); | 287 | glClear(GL_COLOR_BUFFER_BIT); |
| @@ -231,33 +289,18 @@ int main(int argc, char **argv) { | |||
| 231 | // シェーダープログラムを使用 | 289 | // シェーダープログラムを使用 |
| 232 | glUseProgram(shaderProgram); | 290 | glUseProgram(shaderProgram); |
| 233 | 291 | ||
| 234 | // 時間に基づいて色をアニメーション | 292 | // マウスの位置(ピクセル、左下が原点になる様にyを反転) |
| 235 | float time = (float)glfwGetTime(); | 293 | double mouseX, mouseY; |
| 236 | float speed = 1.0f; // アニメーションの速度 | 294 | glfwGetCursorPos(window, &mouseX, &mouseY); |
| 237 | float phase = 2.0f * 3.14159f / 3.0f; // // 120度の位相差(3色用) | 295 | int windowW, windowH; |
| 238 | 296 | glfwGetWindowSize(window, &windowW, &windowH); | |
| 239 | // uColorA: 赤 -> 緑 -> 青 | 297 | float scale = windowW > 0 ? (float)gWidth / (float)windowW : 1.0f; |
| 240 | Vector3 colorA; | ||
| 241 | colorA.r = sin(time * speed + 0.0f) * 0.5f + 0.5f; // 0.0 to 1.0 | ||
| 242 | colorA.g = sin(time * speed + phase) * 0.5f + 0.5f; | ||
| 243 | colorA.b = sin(time * speed + 2.0f * phase) * 0.5f + 0.5f; | ||
| 244 | |||
| 245 | // uColorB: 緑 -> 青 -> 赤 | ||
| 246 | Vector3 colorB; | ||
| 247 | colorB.r = sin(time * speed + 2.0f * phase) * 0.5f + 0.5f; | ||
| 248 | colorB.g = sin(time * speed + 0.0f) * 0.5f + 0.5f; | ||
| 249 | colorB.b = sin(time * speed + phase) * 0.5f + 0.5f; | ||
| 250 | |||
| 251 | // uColorC: 青 -> 赤 -> 緑 | ||
| 252 | Vector3 colorC; | ||
| 253 | colorC.r = sin(time * speed + phase) * 0.5f + 0.5f; | ||
| 254 | colorC.g = sin(time * speed + 2.0f * phase) * 0.5f + 0.5f; | ||
| 255 | colorC.b = sin(time * speed + 0.0f) * 0.5f + 0.5f; | ||
| 256 | 298 | ||
| 257 | // ユニフォームを設定 | 299 | // ユニフォームを設定 |
| 258 | glUniform3f(glGetUniformLocation(shaderProgram, "uColorA"), colorA.r, colorA.g, colorA.b); // 赤 | 300 | glUniform1f(glGetUniformLocation(shaderProgram, "uTime"), (float)glfwGetTime()); |
| 259 | glUniform3f(glGetUniformLocation(shaderProgram, "uColorB"), colorB.r, colorB.g, colorB.b); // 緑 | 301 | glUniform2f(glGetUniformLocation(shaderProgram, "uResolution"), (float)gWidth, (float)gHeight); |
| 260 | glUniform3f(glGetUniformLocation(shaderProgram, "uColorC"), colorC.r, colorC.g, colorC.b); // 青 | 302 | glUniform2f(glGetUniformLocation(shaderProgram, "uMouse"), |
| 303 | (float)mouseX * scale, (float)(windowH - mouseY) * scale); | ||
| 261 | 304 | ||
| 262 | // クアッドを描画 | 305 | // クアッドを描画 |
| 263 | glBindVertexArray(VAO); | 306 | glBindVertexArray(VAO); |
ファイル: /dev/null | b/shader/playground.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,22 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | in vec2 vUv; | ||
| 4 | out vec4 out_color; | ||
| 5 | |||
| 6 | uniform float uTime; // 起動してからの秒数 | ||
| 7 | uniform vec2 uResolution; // 画面の大きさ(ピクセル) | ||
| 8 | uniform vec2 uMouse; // マウスの位置(ピクセル、左下が原点) | ||
| 9 | |||
| 10 | void main() { | ||
| 11 | // ピクセル座標を0〜1に正規化 | ||
| 12 | vec2 uv = gl_FragCoord.xy / uResolution; | ||
| 13 | |||
| 14 | // 時間で動くグラデーション | ||
| 15 | vec3 color = 0.5 + 0.5 * cos(uTime + uv.xyx + vec3(0.0, 2.0, 4.0)); | ||
| 16 | |||
| 17 | // マウスの周りに白い円 | ||
| 18 | float d = length(gl_FragCoord.xy - uMouse); | ||
| 19 | color = mix(color, vec3(1.0), 1.0 - smoothstep(18.0, 20.0, d)); | ||
| 20 | |||
| 21 | out_color = vec4(color, 1.0); | ||
| 22 | } | ||
コメント(0)
まだコメントはありません。
ログインするとコメント出来ます。