15. ジオメトリ・テッセレーション・コンピュートシェーダー
此れまで書いたシェーダーは2種類です。
頂点シェーダーとフラグメントシェーダーです。
全てのプログラムに必要なのは此の2つですが、OpenGLには更に4つあります。
ジオメトリシェーダー、2つのテッセレーションシェーダー(制御と評価)、そしてコンピュートシェーダーです。
第2章では数行で説明しただけでしたが、此の最後の章では其々に小さな動くプログラムを書きます。
- メッシュの法線を線で描くジオメトリシェーダーと、球を爆発させるジオメトリシェーダー。
- 64個の粗い四角形の格子を、カメラに近い程細かい、ノイズで変位させた詳細な地形にするテッセレーションのパイプライン。
- 200×150の格子でコンウェイのライフゲームを動かし、結果をテクスチャに書くコンピュートシェーダー。
メッシュ、行列、ライティングが必要なので、第14章のプレイグラウンドではなく第13章のコードの続きから始めます。
4つのデモは全て1つのプログラムの中にあり、1〜4キーで切り替えます。
macOS:ジオメトリシェーダー(OpenGL 3.2)とテッセレーション(OpenGL 4.0)はmacOSのOpenGL 4.1で動きます。
コンピュートシェーダーにはOpenGL 4.3が必要なので、デモ4はmacOSでは使えません。コードはmacOSでは其れを飛ばし、4キーを押しても何も起きません。
15.1 他のステージの位置
此れが、まだ使っていないステージを含めたOpenGLのパイプライン全体です:
- テッセレーションのステージは頂点シェーダーの直ぐ後に来ます。パッチ(頂点の小さなグループ)を受け取り、沢山の三角形に分割します。真ん中のテッセレーターは固定のハードウェアで、あたし達はどれ位細かく切るか(制御シェーダー)と、新しい頂点を其々何処に置くか(評価シェーダー)を伝えるだけです。
- 次がジオメトリシェーダーです。プリミティブ(三角形、線、点)全体を其の全ての頂点と一緒に1つずつ見て、好きな数の新しいプリミティブを出力出来ます。違う種類でも、1つも出力しなくても構いません。
- コンピュートシェーダーは描画のパイプラインの一部ではありません。GPU上で沢山の並列なコピーとして動く汎用のプログラムで、バッファや画像を読み書きし、其れ自体は何も描きません。
ラスタライザーの前の最後のステージがgl_Positionを書かなければなりません。
テッセレーションやジオメトリシェーダーがあると、頂点シェーダーはもう投影をする必要がありません。
両方のデモで其れを見ます。
15.2 Cのコードの準備
好きなステージの組み合わせのプログラム
第5章のbuildProgramは、丁度2つのファイルしか知りません。
一般化します。
ステージ毎に1つ、6つのパスの配列で、NULLは「使わない」という意味です:
// シェーダーファイルを読み込んでコンパイルする(失敗したら0)
GLuint compileShaderFile(GLenum type, const char *path) {
char *source = readShaderFile(path);
if (!source) return 0;
#ifdef __APPLE__
// macOSはOpenGL 4.1までなので、「#version 450」を「#version 410」に書き換える
char *v;
if ((v = strstr(source, "#version 450"))) v[10] = '1';
#endif
GLuint shader = compileShader(type, source);
free(source);
return shader;
}
// シェーダーステージの種類(パスの配列の順番)
enum { STAGE_VERTEX, STAGE_TESS_CONTROL, STAGE_TESS_EVALUATION, STAGE_GEOMETRY, STAGE_FRAGMENT,
STAGE_COMPUTE, STAGE_COUNT };
static const GLenum gStageTypes[STAGE_COUNT] = {
GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER,
GL_FRAGMENT_SHADER,
#ifdef __APPLE__
0, // macOS(OpenGL 4.1)にはコンピュートシェーダーがない
#else
GL_COMPUTE_SHADER,
#endif
};
// 好きなステージの組み合わせからプログラムを作る(NULLのステージは使わない、失敗したら0)
GLuint buildProgramStages(const char *const paths[STAGE_COUNT]) {
GLuint shaders[STAGE_COUNT] = { 0 };
int ok = 1;
for (int i = 0; i < STAGE_COUNT; i++) {
if (!paths[i]) continue;
if (!gStageTypes[i]) {
printf("此のプラットフォームでは使えないシェーダーです: %s\n", paths[i]);
ok = 0;
break;
}
shaders[i] = compileShaderFile(gStageTypes[i], paths[i]);
if (!shaders[i]) {
ok = 0;
break;
}
}
GLuint program = 0;
if (ok) {
program = glCreateProgram();
for (int i = 0; i < STAGE_COUNT; i++) {
if (shaders[i]) glAttachShader(program, shaders[i]);
}
glLinkProgram(program);
GLint success;
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
checkProgramLink(program);
glDeleteProgram(program);
program = 0;
}
}
for (int i = 0; i < STAGE_COUNT; i++) {
if (shaders[i]) glDeleteShader(shaders[i]);
}
return program;
}
// 頂点シェーダーとフラグメントシェーダーのファイルからプログラムを作る(失敗したら0)
GLuint buildProgram(const char *vertexPath, const char *fragmentPath) {
const char *paths[STAGE_COUNT] = { NULL };
paths[STAGE_VERTEX] = vertexPath;
paths[STAGE_FRAGMENT] = fragmentPath;
return buildProgramStages(paths);
}
本当の検査はリンクが行います。
あるステージの出力が次のステージの入力と合わない場合や、コンピュートシェーダーを描画のステージと混ぜた場合(コンピュートのプログラムにはコンピュートシェーダーだけが入ります)に文句を言います。
buildProgramは薄いラッパーとして、前と同じ様に使えます。
プログラムの表
第13章の表は、プログラム毎に6つのパスを持つ様になり、新しい項目が5つ増えます:
// シェーダープログラムの一覧(Rキーで全部再読み込み)
typedef struct {
const char *paths[STAGE_COUNT]; // 頂点、テッセレーション制御、テッセレーション評価、ジオメトリ、フラグメント、コンピュート
GLuint id;
} Program;
enum {
PROGRAM_SKY, PROGRAM_PHONG, PROGRAM_GOURAUD, PROGRAM_FLAG,
PROGRAM_NORMALS, PROGRAM_EXPLODE, PROGRAM_TERRAIN, PROGRAM_LIFE_STEP, PROGRAM_LIFE_SHOW,
PROGRAM_COUNT
};
static Program gPrograms[PROGRAM_COUNT] = {
{ { "shader/vertex_fullscreen.glsl", NULL, NULL, NULL, "shader/frag_sky.glsl", NULL }, 0 },
{ { "shader/vertex_mesh.glsl", NULL, NULL, NULL, "shader/frag_phong.glsl", NULL }, 0 },
{ { "shader/vertex_gouraud.glsl", NULL, NULL, NULL, "shader/frag_gouraud.glsl", NULL }, 0 },
{ { "shader/vertex_flag.glsl", NULL, NULL, NULL, "shader/frag_phong.glsl", NULL }, 0 },
// ジオメトリシェーダー:法線を線で描く、爆発
{ { "shader/vertex_passthrough.glsl", NULL, NULL, "shader/geom_normals.glsl", "shader/frag_line.glsl",
NULL }, 0 },
{ { "shader/vertex_passthrough.glsl", NULL, NULL, "shader/geom_explode.glsl", "shader/frag_phong.glsl",
NULL }, 0 },
// テッセレーション:地形
{ { "shader/vertex_terrain.glsl", "shader/tesc_terrain.glsl", "shader/tese_terrain.glsl", NULL,
"shader/frag_terrain.glsl", NULL }, 0 },
// コンピュートシェーダー:ライフゲーム(macOSでは使えない)
{ { NULL, NULL, NULL, NULL, NULL, "shader/comp_life.glsl" }, 0 },
{ { "shader/vertex_fullscreen.glsl", NULL, NULL, NULL, "shader/frag_life.glsl", NULL }, 0 },
};
loadProgramsは前と同じ様に表をループし、buildProgramStages(gPrograms[i].paths)を呼びます。
macOSではライフゲームの2つのプログラムを飛ばします。
第13章の描画のコードはdrawSceneDemoという関数に移し、新しいデモにも其々専用の関数を作りました。
メインループでは数字キーでデモを選びます:
// 1〜4キーでデモを切り替える
for (int i = 0; i < 4; i++) {
if (glfwGetKey(window, GLFW_KEY_1 + i) == GLFW_PRESS && demo != i) {
#ifdef __APPLE__
if (i == 3) continue; // コンピュートシェーダーはmacOSでは使えない
#endif
demo = i;
glfwSetWindowTitle(window, demoNames[i]);
puts(demoNames[i]);
}
}
GLFW_KEY_1〜GLFW_KEY_4は連続したコード(49〜52、ASCIIの数字)なので、GLFW_KEY_1 + iで動きます。
glfwSetWindowTitleは今のデモをタイトルバーに表示します。
デモの関数から使える様に、メッシュはグローバル変数(gCube、gSphere、…)になり、drawSky()は第13章の背景を描きます。
main.c全体は章の最後のDIFFにあります。
15.3 ジオメトリシェーダー
何を見て何をするか
ジオメトリシェーダーはプリミティブ毎に1回実行されます。
三角形なら、3つの頂点の出力を一度に配列として受け取り、新しい頂点を1つずつ出力します:
layout(triangles) in;は入力の種類を宣言します。points、lines、triangles(又は_adjacency版)。描画呼び出しが作る物と合っていなければなりません。layout(line_strip, max_vertices = 6) out;は出力の種類(points、line_strip又はtriangle_strip)と、1回の実行で出力出来る頂点の最大数を宣言します。GPUは其の最大数分のメモリを確保するので、小さくしておきます。- 頂点シェーダーからの入力変数は全て配列になります。
in vec3 vsNormal[];で、プリミティブの頂点毎に1つの要素があります(三角形なら3つ)。組み込みのgl_in[i].gl_Positionには、頂点シェーダーが書いた場合、其のgl_Positionが入っています。 - 頂点を出力するには、全ての出力変数と
gl_Positionを設定してからEmitVertex()を呼びます。値はコピーされ、呼び出しの後は全ての出力が未定義になるので、次のEmitVertex()の前に全ての出力をもう一度設定します。 EndPrimitive()は今のストリップを終わらせます。次に出力する頂点から新しい線や三角形のストリップが始まります。
デモ2a:法線を描く
此れは定番のデバッグ道具です。
ジオメトリシェーダーは各三角形を受け取り、其れを描く代わりに、各頂点から法線に沿った短い線を3本出力します。
投影はジオメトリシェーダーが行うので、頂点シェーダーはデータを其のまま渡すだけです:
#version 450 core
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aUv;
// 何も変換せず、そのままジオメトリシェーダーへ渡す
out vec3 vsPosition;
out vec3 vsNormal;
out vec2 vsUv;
void main() {
vsPosition = aPosition;
vsNormal = aNormal;
vsUv = aUv;
}
gl_Positionを全く書いていません。
其れで良いのです。
書かなければならないのはラスタライザーの前の最後のステージだけです。
#version 450 core
layout(triangles) in; // 入力:三角形(頂点3つ)
layout(line_strip, max_vertices = 6) out; // 出力:線(頂点2つ × 3本)
in vec3 vsPosition[]; // 頂点シェーダーの出力は配列になる
in vec3 vsNormal[];
out vec3 gColor;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProj;
uniform mat3 uNormalMatrix;
uniform float uLength; // 線の長さ(ワールド空間)
void main() {
mat4 viewProj = uProj * uView;
for (int i = 0; i < 3; i++) {
vec3 p = (uModel * vec4(vsPosition[i], 1.0)).xyz;
vec3 n = normalize(uNormalMatrix * vsNormal[i]);
gColor = vec3(1.0, 0.9, 0.2); // 根元:黄色
gl_Position = viewProj * vec4(p, 1.0);
EmitVertex();
gColor = vec3(1.0, 0.2, 0.1); // 先:赤
gl_Position = viewProj * vec4(p + n * uLength, 1.0);
EmitVertex();
EndPrimitive(); // 1本の線が終わり
}
}
線はワールド空間で作ります。
とを、同じビュー・投影行列で投影します。
こうすると、物体の大きさに関係なく、全ての線が本当に0.2単位の長さになります。
フラグメントシェーダーは補間された色を出力するだけです:
#version 450 core
in vec3 gColor;
out vec4 out_color;
void main() {
out_color = vec4(gColor, 1.0);
}
Cでは、潰した球をフォンのプログラムで普通に描き、其の上に法線のプログラムで同じメッシュをもう一度描きます:
// 右:普通に描いてから、同じメッシュの法線を線で重ねる
Matrix4 rightModel = mat4Multiply(mat4Translate(1.3f, 0.1f, 0.0f),
mat4Multiply(mat4RotateY(time * 0.4f), mat4Scale(0.9f, 0.6f, 0.9f)));
GLuint lit = gPrograms[PROGRAM_PHONG].id;
setCommonUniforms(lit, view, proj, eye, time);
Vector3 rightColor = { { 0.2f, 0.45f, 0.9f } };
drawLit(lit, &gSphere, rightModel, rightColor, 0);
GLuint normals = gPrograms[PROGRAM_NORMALS].id;
setCommonUniforms(normals, view, proj, eye, time);
glUniform1f(glGetUniformLocation(normals, "uLength"), 0.2f);
drawLit(normals, &gSphere, rightModel, rightColor, 0);
描画呼び出しはglDrawElements(GL_TRIANGLES, ...)のままです。
途中でジオメトリシェーダーが三角形を線に変えます。
各頂点は幾つもの三角形で共有されているので、殆どの法線は2回以上描かれますが、デバッグ表示なら問題ありません。
此れは正に第13章の確認です。
潰した球でも、法線行列を使っているので線は表面に垂直に立ちます。
デモ2b:爆発する球
2つ目のジオメトリシェーダーは三角形を残しますが、其々を自分の面の法線に沿って外側へ動かすので、表面がバラバラの欠片に割れます。
面の法線は2つの辺の外積から求めます(線型代数の第5章):
反時計回りの巻き順(第13章)なら、此れはメッシュの外を向きます。
+Zから見て反時計回りの三角形で確かめると、で、見ている人の方を向きます。
#version 450 core
layout(triangles) in;
layout(triangle_strip, max_vertices = 3) out;
in vec3 vsPosition[];
in vec3 vsNormal[];
in vec2 vsUv[];
// フラグメントシェーダー(frag_phong.glsl)が受け取る名前
out vec3 vWorldPos;
out vec3 vNormal;
out vec2 vUv;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProj;
uniform mat3 uNormalMatrix;
uniform float uExplode; // 押し出す距離
void main() {
vec3 world[3];
for (int i = 0; i < 3; i++) world[i] = (uModel * vec4(vsPosition[i], 1.0)).xyz;
// 面の法線:二つの辺の外積(線型代数の第5章)
vec3 faceNormal = normalize(cross(world[1] - world[0], world[2] - world[0]));
vec3 offset = faceNormal * uExplode;
mat4 viewProj = uProj * uView;
for (int i = 0; i < 3; i++) {
vWorldPos = world[i] + offset;
vNormal = uNormalMatrix * vsNormal[i];
vUv = vsUv[i];
gl_Position = viewProj * vec4(vWorldPos, 1.0);
EmitVertex();
}
EndPrimitive();
}
ジオメトリシェーダーの出力は、第13章の頂点シェーダーと全く同じvWorldPos、vNormal、vUvという名前なので、frag_phong.glslに手を付けずに再利用出来ます。
フラグメントシェーダーが気にするのは届く物の名前と型だけで、どのステージが作ったかは関係ありません。
Cでは押し出す量を0〜0.5でアニメーションさせ、隙間から球の内側が見えるのでカリングを無効にします:
// 爆発:三角形を面の法線の方向へ押し出す(裏側も見えるのでカリングなし)
GLuint explode = gPrograms[PROGRAM_EXPLODE].id;
setCommonUniforms(explode, view, proj, eye, time);
float amount = 0.5f * (0.5f - 0.5f * cosf(time * 1.2f)); // 0〜0.5を行ったり来たり
glUniform1f(glGetUniformLocation(explode, "uExplode"), amount);
Matrix4 leftModel = mat4Multiply(mat4Translate(-1.3f, 0.1f, 0.0f),
mat4Multiply(mat4RotateY(time * 0.4f), mat4Scale(0.8f, 0.8f, 0.8f)));
Vector3 leftColor = { { 0.9f, 0.3f, 0.35f } };
glDisable(GL_CULL_FACE);
drawLit(explode, &gSmoothSphere, leftModel, leftColor, 1);
glEnable(GL_CULL_FACE);
欠片は三角形ではなく小さな四角形に見えます。
UV球の各四角形の2つの三角形は面の法線が殆ど同じなので、一緒に飛んで行くからです。
gSmoothSphereはcreateSphere(32, 24)で、第13章の球より細かいので欠片も多くなります。
何故ジオメトリシェーダーは避けられる事が多いか
ジオメトリシェーダーは完璧な道具に見えますが、実際には控えめに使われます。
出力は順番通りに出なければならず、其の大きさはシェーダーが動いている間にしか分からないので、GPUは其れをバッファリングしなければならず、多くのGPUでは数個以上の頂点を出力するジオメトリシェーダーは遅いです。
殆どの仕事には今はもっと速い道具があります。
メッシュの沢山のコピーにはインスタンシング、生成するジオメトリには頂点バッファに書き込むコンピュートシェーダー、そして新しいハードウェアでは頂点・テッセレーション・ジオメトリのステージを丸ごと置き換えるメッシュシェーダー(VulkanとDirectX 12にあり、コアOpenGLにはありません)です。
上の法線の様なデバッグ表示、ワイヤーフレームの重ね描き、シャドウボリュームの押し出し、1回のパスでキューブマップの複数の面に描く事(gl_Layerを使う)、点をカメラを向いた四角形にする事等の小さな効果には、今でも便利です。
15.4 テッセレーション
パッチと2つのシェーダー
テッセレーションでは、描画呼び出しはもう三角形ではなくパッチを送ります。
決まった数の頂点のグループで、意味はあたし達が自由に決めます。
此処ではパッチは地形の格子の四角形1つ、4つの角です。
glPatchParameteri(GL_PATCH_VERTICES, 4); // 1パッチ=4頂点
glBindVertexArray(gTerrain.vao);
glDrawElements(GL_PATCHES, gTerrain.indexCount, GL_UNSIGNED_INT, 0);
格子はcreatePatchGrid(8, 10.0f)で作ります。
xz平面上で原点が中心の10 × 10単位を覆う8 × 8個の四角形で、四角形毎にの順に4つのインデックスがあります(関数はDIFFにあります。第13章のcreateGridと同じ仕組みですが、xz平面上で、セル毎に6つではなく4つのインデックスです)。
パッチは僅か64個、頂点は81個で、残りはテッセレーターが作ります。
仕事は3つに分かれます:
- テッセレーション制御シェーダー(TCS)は、各パッチの出力制御点毎に1回実行されます。制御点を其のまま渡すか変更し、
gl_TessLevelOuterとgl_TessLevelInnerでパッチをどれ位細かく分割するかを決めます。 - 固定のテッセレーターは、抽象的なパッチの領域(此処では単位正方形)を沢山の小さな三角形に切り、新しい頂点毎に其の領域の中の位置
gl_TessCoordを作ります。 - テッセレーション評価シェーダー(TES)は新しい頂点毎に1回実行され、
gl_TessCoordとパッチの制御点から本当の位置を求め、gl_Positionを書きます。
テッセレーションレベル
四角形のパッチには、辺毎に1つ、合わせて4つの外側のレベルと、2つの内側のレベルがあります:
gl_TessLevelOuter[0]〜[3]:各辺を何個に切るか。辺0は、辺1は、辺2は、辺3はです。gl_TessLevelInner[0]と[1]:内側を方向と方向にどれ位細かく切るか。
外側のレベルが分かれているのには、とても良い理由があります。
隣り合う2つのパッチは辺を共有していて、其の辺を違う数に切ると頂点が揃わず、表面にひび割れが出来ます。
両方のパッチが共有する辺に同じ外側のレベルを計算する限り、内側のレベルが何であっても表面は隙間なく繋がります。
頂点シェーダーと制御シェーダー
頂点シェーダーは角を渡すだけです:
#version 450 core
layout(location = 0) in vec3 aPosition;
out vec3 vsPosition; // パッチの角(ワールド空間)
void main() {
vsPosition = aPosition;
}
制御シェーダーはカメラまでの距離からレベルを決めます:
#version 450 core
layout(vertices = 4) out; // 出力パッチの制御点の数
in vec3 vsPosition[];
out vec3 tcPosition[];
uniform vec3 uCameraPos;
// 辺の中点がカメラに近い程細かくする
float edgeLevel(vec3 a, vec3 b) {
float d = distance(0.5 * (a + b), uCameraPos);
return clamp(96.0 / d, 1.0, 64.0);
}
void main() {
// 制御点はそのまま渡す(gl_InvocationIDは0〜3)
tcPosition[gl_InvocationID] = vsPosition[gl_InvocationID];
// 分割数はパッチに1回だけ決めれば良い
if (gl_InvocationID == 0) {
// 角の順番:0 = (0, 0)、1 = (1, 0)、2 = (1, 1)、3 = (0, 1)
gl_TessLevelOuter[0] = edgeLevel(vsPosition[3], vsPosition[0]); // u = 0の辺
gl_TessLevelOuter[1] = edgeLevel(vsPosition[0], vsPosition[1]); // v = 0の辺
gl_TessLevelOuter[2] = edgeLevel(vsPosition[1], vsPosition[2]); // u = 1の辺
gl_TessLevelOuter[3] = edgeLevel(vsPosition[2], vsPosition[3]); // v = 1の辺
gl_TessLevelInner[0] = max(gl_TessLevelOuter[1], gl_TessLevelOuter[3]); // u方向
gl_TessLevelInner[1] = max(gl_TessLevelOuter[0], gl_TessLevelOuter[2]); // v方向
}
}
layout(vertices = 4) out;は、TCSがパッチ毎に出力する制御点の数、つまりパッチ毎に何回実行されるかを指定します。gl_InvocationIDは各実行に、どれを担当するかを教えます。- 入力と出力はパッチ全体の配列です。
vsPosition[](4つの入力点は全ての実行から読めます)、tcPosition[](各実行は自分の要素だけに書くべきです)。 - レベルはパッチ毎なので、1つの実行が設定します。
辺のレベルは其の辺の中点だけで決まり、其の辺を共有する2つのパッチは同じ中点を見るので、常に一致します。
ひび割れは出来ません。
例えば中点がカメラから8単位の辺は個に、16単位なら6個に分けられ、1.5単位より近ければ最大値の64になります。
(64は全てのOpenGL 4のGPUが対応する最大値の最低限、GL_MAX_TESS_GEN_LEVELです。)
評価シェーダー
#version 450 core
layout(quads, fractional_odd_spacing, ccw) in;
in vec3 tcPosition[];
out vec3 vWorldPos;
out vec3 vNormal;
uniform mat4 uView;
uniform mat4 uProj;
// 第8章の様なバリューノイズとfBm
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float valueNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float sum = 0.0;
float amplitude = 0.5;
for (int i = 0; i < 4; i++) {
sum += amplitude * valueNoise(p);
p *= 2.0;
amplitude *= 0.5;
}
return sum;
}
// 地形の高さ(0より下は水面で平らにする)
float terrainHeight(vec2 xz) {
return max(3.0 * fbm(xz * 0.3) - 1.1, 0.0);
}
void main() {
// gl_TessCoord:パッチの中の位置(u, v)、0〜1
vec2 uv = gl_TessCoord.xy;
vec3 bottom = mix(tcPosition[0], tcPosition[1], uv.x);
vec3 top = mix(tcPosition[3], tcPosition[2], uv.x);
vec3 p = mix(bottom, top, uv.y);
// 高さで押し上げる(ディスプレイスメント)
p.y = terrainHeight(p.xz);
// 法線:高さの差分から(y = h(x, z) の勾配)
const float e = 0.02;
float hx = terrainHeight(p.xz + vec2(e, 0.0)) - terrainHeight(p.xz - vec2(e, 0.0));
float hz = terrainHeight(p.xz + vec2(0.0, e)) - terrainHeight(p.xz - vec2(0.0, e));
vNormal = normalize(vec3(-hx, 2.0 * e, -hz));
vWorldPos = p;
gl_Position = uProj * uView * vec4(p, 1.0);
}
layoutの行はテッセレーターを設定します:
| 部分 | 選択肢 | 意味 |
|---|---|---|
| 領域 | triangles、quads、isolines |
パッチの領域の形とgl_TessCoordの意味 |
| 間隔 | equal_spacing、fractional_even_spacing、fractional_odd_spacing |
整数でないレベルの扱い方 |
| 巻き順 | ccw、cw |
生成される三角形の巻き順(カリング用) |
equal_spacingでは5.3のレベルは6に切り上げられ、カメラが動くと分割が急に飛びます。
fractionalの方式は代わりに辺の真ん中から新しい分割を徐々に育てるので、詳細の変化が滑らかです。
だから地形には普通此方を使います。
パッチの中の位置は4つの角の双線型補間です。
先ず下と上の辺で方向に、次に其の2点の間を方向に補間します(mixを2段階、第11章のバイリニアフィルタリングと同じです)。
例えば角が、、、のパッチでなら、下 、上 で、mixの結果はです。
次に第8章のfBmノイズ(4オクターブ)から高さを求めます。
max(..., 0.0)は0より下を全て平らな湖にします。
法線は第14章と同じく中心差分から求めます。
表面は、つまりで、其の勾配はです。
(はコードの差分hx)なので、ベクトル全体にを掛けるとになり、方向は同じで割り算が2回減ります。
フラグメントシェーダーは第6章のsmoothstepを使って高さと傾きで色を付け(水、砂、草、岩、雪)、拡散光と霧を加えます:
#version 450 core
in vec3 vWorldPos;
in vec3 vNormal;
out vec4 out_color;
uniform vec3 uLightDir;
uniform vec3 uCameraPos;
void main() {
vec3 n = normalize(vNormal);
float h = vWorldPos.y;
// 高さと傾きで色を決める
vec3 rock = vec3(0.3, 0.27, 0.25);
vec3 color = vec3(0.05, 0.15, 0.35); // 水
color = mix(color, vec3(0.6, 0.55, 0.35), smoothstep(0.0, 0.05, h)); // 砂
color = mix(color, vec3(0.12, 0.35, 0.08), smoothstep(0.1, 0.3, h)); // 草
color = mix(color, rock, smoothstep(0.55, 0.8, h)); // 岩
color = mix(color, rock, 1.0 - smoothstep(0.6, 0.75, n.y)); // 急な斜面も岩
color = mix(color, vec3(0.9), smoothstep(1.1, 1.3, h)); // 雪
float diffuse = max(dot(n, normalize(uLightDir)), 0.0);
vec3 lit = color * (0.15 + diffuse);
// 遠い程空の色に
float d = distance(vWorldPos, uCameraPos);
float fog = 1.0 - exp(-0.002 * d * d);
lit = mix(lit, vec3(0.32, 0.30, 0.36), fog);
out_color = vec4(pow(lit, vec3(1.0 / 2.2)), 1.0);
}
「平らな程岩が少ない」を表す1.0 - smoothstep(0.6, 0.75, n.y)に注目して下さい。
代わりにsmoothstep(0.75, 0.6, n.y)と書きたくなり、多くのドライバーでは期待通りの結果になる事さえありますが、GLSLの仕様では、最初の端が2番目より小さくない場合の結果は未定義です。
Wキーを押すとワイヤーフレームになります(第13章と同じくglPolygonModeを使います):
カメラの近くでは三角形が小さく、奥へ行く程大きくなります。
カメラは地形の周りをゆっくり回り、動くにつれて、詳細が必要な所へ滑らかに流れて行きます。
メモリの中の64個の四角形から何千もの三角形が出来、CPUは其の何もする必要がありませんでした。
15.5 コンピュートシェーダー
違う考え方
コンピュートシェーダーは頂点やピクセルに結び付いていません。
幾つのコピーを実行するかを自分で決め、各コピーは自分がどれかを知り、仕事の自分の分担をします。
コピーは2段階で組織されます:
- ワークグループは、大きさがシェーダーの中で固定された呼び出しのブロックです。
layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;は1グループに16 × 16 = 256個の呼び出しという意味です。1つのグループの呼び出しは一緒に実行され、メモリを共有(shared変数)し、同期(barrier())出来ます。 - Cの
glDispatchCompute(x, y, z)は個のワークグループの格子を開始します。
各呼び出しには組み込みのIDがあります:
(成分毎、uvec3として)。
2Dの画像なら、ピクセル毎に1つの呼び出しにするのが自然なので、gl_GlobalInvocationID.xyが其のままピクセルの座標になります。
画像
フラグメントシェーダーはtexture()でテクスチャを読みますが、コンピュートシェーダーは普通画像を使います。
画像はフィルタリングなしで、正確な整数の座標で読み書き出来るテクスチャです:
layout(rgba8, binding = 0) uniform readonly image2D uCurrent;は、画像ユニット0にあるrgba8形式(テクスチャの形式と一致しなければなりません)の画像を宣言し、読むだけだと約束します。imageLoad(image, ivec2)はテクセルを1つ読み、imageStore(image, ivec2, vec4)は1つ書き、imageSize(image)は大きさをivec2で返します。rgba8の画像では、値はテクスチャと同じく0〜1のfloatです。
デモ4:コンウェイのライフゲーム
ライフゲームはセルの格子で、各セルは生きているか死んでいるかです。
世代毎に、各セルは8つの隣を数えます:
- 生きているセルは、生きている隣が2つか3つなら生き残り、其れ以外なら死にます。
- 死んでいるセルは、生きている隣が丁度3つなら生まれます。
有名な「ブリンカー」で仕組みが分かります。
横一列に並んだ3つの生きているセルです。
真ん中は隣が2つで生き残り、両端は1つしか無いので死に、真ん中の真上と真下のセルは3つ(列全体)なので生まれます。
従って横の線は縦の線になり、次の世代で元に戻ります。
全てのセルは前の世代から独立に計算出来るので、GPUにぴったりの仕事です。
1つ罠があります。
同じ画像を読み書きすると、あるセルが既に次の世代に更新された隣を読んでしまう可能性があります。
其の為、テクスチャを2つ使い、毎ステップ入れ替えます(ピンポン)。
一方から読み、もう一方に書きます。
#version 450 core
// 1つのワークグループ=16 × 16個の呼び出し
layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;
layout(rgba8, binding = 0) uniform readonly image2D uCurrent; // 今の世代
layout(rgba8, binding = 1) uniform writeonly image2D uNext; // 次の世代
void main() {
ivec2 size = imageSize(uCurrent);
ivec2 p = ivec2(gl_GlobalInvocationID.xy); // 此の呼び出しが担当するセル
if (p.x >= size.x || p.y >= size.y) return; // 端のワークグループのはみ出し
// 周りの8つのセルを数える(端は反対側に繋がる)
int neighbours = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
if (dx == 0 && dy == 0) continue;
ivec2 q = (p + ivec2(dx, dy) + size) % size;
if (imageLoad(uCurrent, q).r > 0.5) neighbours++;
}
}
// ライフゲームの規則:生きているセルは2か3で生き残り、死んでいるセルは3で生まれる
vec4 cell = imageLoad(uCurrent, p);
bool alive = cell.r > 0.5;
bool next = alive ? (neighbours == 2 || neighbours == 3) : (neighbours == 3);
// g:残像(死んだ後ゆっくり消える)
float trail = next ? 1.0 : cell.g * 0.92;
imageStore(uNext, p, vec4(next ? 1.0 : 0.0, trail, 0.0, 1.0));
}
(p + ivec2(dx, dy) + size) % sizeは端で反対側に繋げるので、格子はドーナツの表面の様に振る舞います。先にsizeを足すのは値を正にする為です。GLSLでは負の整数の%は未定義です。- 赤のチャンネルは生(1)か死(0)を、緑は世代毎に8%ずつ消える残像を持ちます(見た目の為だけです)。
格子は200 × 150、ワークグループは16 × 16なので、横に個、縦に個のグループが必要です。
Cの整数の計算で切り上げるには(n + 15) / 16です。
、。
此れで個の呼び出しが起動し、セルより少し多いですが、先頭のreturnで余った物は何もしません:
C側
2つのテクスチャはglTexStorage2D(OpenGL 4.2)で作ります。
大きさと形式を一度に固定します(イミュータブルなテクスチャ)。
glBindImageTexture用のテクスチャは此の様に作るのがお勧めです(デスクトップのOpenGLは形式が合っていて完全なglTexImage2Dのテクスチャも受け付けますが、OpenGL ESはイミュータブルな物だけです):
void createLife(void) {
glGenTextures(2, gLifeTextures);
for (int i = 0; i < 2; i++) {
glBindTexture(GL_TEXTURE_2D, gLifeTextures[i]);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, LIFE_WIDTH, LIFE_HEIGHT); // 大きさと形式を固定
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
resetLife(76);
}
resetLifeは小さな線型合同法とglTexSubImage2Dで今のテクスチャをランダムなセル(約25%が生)で埋め、スペースキーを押した時にも呼ばれます。
シミュレーションの1ステップ:
// 1世代進める:今のテクスチャを読み、もう一方に書く
void stepLife(void) {
GLuint program = gPrograms[PROGRAM_LIFE_STEP].id;
glUseProgram(program);
glBindImageTexture(0, gLifeTextures[gLifeCurrent], 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, gLifeTextures[1 - gLifeCurrent], 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
// ワークグループは16 × 16なので、切り上げて画像全体を覆う
glDispatchCompute((LIFE_WIDTH + 15) / 16, (LIFE_HEIGHT + 15) / 16, 1);
// 書き込みが終わってから次の読み込み(imageLoadとtexture)をする
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT);
gLifeCurrent = 1 - gLifeCurrent;
}
glBindImageTexture(unit, texture, level, layered, layer, access, format)はテクスチャのミップレベル0を画像ユニットに繋ぎます。
ユニットの番号はシェーダーのbinding = 0とbinding = 1に対応します。
glMemoryBarrierは忘れ易い部分です。
画像への書き込みは、パイプラインの普通の自動的に順番が保証される経路を通らないので、OpenGLは次のコマンドが其れを見る事を保証しません。
バリアは「此処より前に書いた物は全て、次の種類の読み込みから見えなければならない」と伝えます。
次の世代のimageLoadの為にGL_SHADER_IMAGE_ACCESS_BARRIER_BIT、結果を描くtexture()の為にGL_TEXTURE_FETCH_BARRIER_BITです。
此れが無いと、自分のGPUでは動いても、別のGPUではゴミが表示されるかもしれません。
結果の表示は、第13章のフルスクリーン三角形と、テクスチャをサンプルするフラグメントシェーダーだけです:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform sampler2D uLife; // コンピュートシェーダーが書いたテクスチャ
void main() {
vec4 cell = texture(uLife, vUv);
vec3 trail = vec3(0.15, 0.1, 0.45) * cell.g; // 残像:紫
vec3 alive = vec3(1.0, 0.9, 0.5) * cell.r; // 生きているセル:明るい黄色
out_color = vec4(max(trail, alive), 1.0);
}
GL_NEARESTのフィルタリングで、各セルは800 × 600のウィンドウ上でくっきりした4 × 4ピクセルの四角形になります。
メインループは1秒に20世代進めます:
if (time - lastLifeStep >= 0.05f) {
stepLife();
lastLifeStep = time;
}
drawComputeDemo();
ランダムなノイズから、数秒でお馴染みの形が現れます。
ブリンカー、ブロック、そして画面を斜めに這って行く(端で反対側に回り込む)グライダーです。
macOSでは、フルスクリーン三角形をフレームバッファのテクスチャに描き、2つのフレームバッファの間でピンポンすれば、コンピュートシェーダーなしで同じ効果が得られます。
其の手法はOpenGLシリーズで扱います。
15.6 何時どれを使うか
| ステージ | 向いている物 | 注意点 |
|---|---|---|
| 頂点シェーダー | 変換、スキニング、頂点アニメーション | 存在する頂点しか動かせない |
| テッセレーション | 地形、ディスプレイスメントマッピング、滑らかな曲面、距離による詳細度 | 隣の辺が一致しないとひび割れ。小さ過ぎる三角形は非効率 |
| ジオメトリシェーダー | デバッグ表示、ワイヤーフレームの重ね描き、レイヤー描画、少量の増幅 | 沢山の頂点を出力すると多くのGPUで遅い |
| フラグメントシェーダー | ライティング、テクスチャ、ポストプロセス、レイマーチング | 全てのピクセルで実行されるので、コストは解像度に比例 |
| コンピュートシェーダー | パーティクル、シミュレーション、カリング、画像処理、描画に結び付かない全て | 明示的なバリアが必要。macOSのOpenGLには無い |
今のエンジンでは、コンピュートシェーダーがどんどん多くの仕事をしています。
見えない物体のカリングと描画リストの作成をGPU上で行い(GPU駆動レンダリング)、パーティクルや布をシミュレートし、タイルやクラスター単位でライティングを計算し、殆どのポストプロセスを実行します。
テッセレーションは地形や近くの詳細に使われますが、メッシュシェーダー(Vulkan、DirectX 12)がテッセレーションとジオメトリシェーダーの両方を徐々に置き換えています。
15.7 此の章の新しいGLSL
| 機能 | ステージ | 内容 |
|---|---|---|
layout(triangles) in; |
ジオメトリ | 入力プリミティブの種類 |
layout(line_strip, max_vertices = N) out; |
ジオメトリ | 出力プリミティブの種類と最大頂点数 |
EmitVertex()、EndPrimitive() |
ジオメトリ | 今の出力を頂点として出力する。ストリップを終わらせる |
gl_in[i].gl_Position |
ジオメトリ、テッセレーション | 頂点iの前のステージのgl_Position |
layout(vertices = N) out; |
テッセレーション制御 | パッチ毎の出力制御点(実行)の数 |
gl_InvocationID |
テッセレーション制御 | 此の実行がどの出力制御点を書くか |
gl_TessLevelOuter[4]、gl_TessLevelInner[2] |
テッセレーション制御 | 辺と内側をどれ位細かく分割するか |
layout(quads, fractional_odd_spacing, ccw) in; |
テッセレーション評価 | テッセレーターの領域、間隔、巻き順 |
gl_TessCoord |
テッセレーション評価 | パッチの領域の中での新しい頂点の位置 |
layout(local_size_x = X, ...) in; |
コンピュート | ワークグループの大きさ |
gl_GlobalInvocationID、gl_LocalInvocationID、gl_WorkGroupID |
コンピュート | 此れがどの呼び出しか |
image2D、layout(rgba8, binding = N) |
コンピュート(どれでも) | 画像ユニット上の画像と其の形式 |
imageLoad、imageStore、imageSize |
コンピュート(どれでも) | 整数の座標で画像を読む、書く、大きさを調べる |
readonly、writeonly |
コンピュート(どれでも) | 画像の使い方を約束するメモリ修飾子 |
15.8 シリーズの終わりと、次に来る物
此れでGLSLシリーズの最後の章です。
長方形の上の2色のグラデーションから始めて、何を作ったか振り返ってみましょう:
- 第1章〜第4章:テスター、言語と方言、グラデーションと補間、三角形と最初のライト。
- 第5章:時間、解像度、マウス、ホットリロード付きのシェーダープレイグラウンド。
- 第6章〜第8章:距離関数による図形、変換とパターン、乱数とノイズ。全て座標だけから。
- 第9章と第10章:鏡面ハイライト、ポイントライト、スポットライト、ヘミスフィアライトとエリアライト、そしてガンマ。
- 第11章と第12章:テクスチャ、フィルタリングとミップマップ、そして法線マップと接線空間。
- 第13章:本物の3Dメッシュ、モデル・ビュー・投影行列、法線行列、頂点アニメーション。
- 第14章:1つのフラグメントシェーダーの中の、影とAO付きの完全な3Dシーン。
- 第15章:パイプラインの他のステージ。
途中で、線型代数シリーズの殆ど全ての考え方がシェーダーに登場しました。
光の為の内積、法線の為の外積、全ての変換の為の行列、逆行列の転置、基底の変換、そして投影です。
詳しく見ていないのはC側です。
main.cは章毎に大きくなり、其の多くの呼び出し(glBufferData、glVertexAttribPointer、glTexStorage2D、glBindImageTexture、…)は、シェーダーに必要な所までしか説明していません。
其れが次のシリーズ、OpenGLの仕事です。
其処では、レンダラーを基礎からちゃんと作ります。
バッファと頂点レイアウト、ユニフォームバッファ、ファイルからの画像とモデルの読み込み、フレームバッファとテクスチャへのレンダリング、シャドウマップ、インスタンシング、そしてAPIの残りの部分を、此のシリーズのシェーダーを部品として使いながら学びます。
其の後にSlangとVulkanのシリーズが続き、此処で覚えたシェーダーが、モダンで明示的なグラフィックスAPIと出会います。
此処まで付いて来てくれて、有難う御座いました。
15.9 まとめ
- 頂点シェーダーとフラグメントシェーダーの他に、OpenGLにはテッセレーション制御、テッセレーション評価、ジオメトリ、コンピュートシェーダーがあります。ラスタライザーの前の最後のステージが
gl_Positionを書きます。 - プログラムは好きなステージの組み合わせから作れます。リンクがインターフェースの一致を検査し、コンピュートのプログラムにはコンピュートシェーダーだけが入ります。
- ジオメトリシェーダーはプリミティブ全体を配列として見て、
EmitVertex()とEndPrimitive()で新しい頂点を出力します。デバッグ表示には最適ですが、重い使い方では遅い事が多いです。 - テッセレーションは
GL_PATCHESを描きます。制御シェーダーがgl_TessLevelOuterとgl_TessLevelInnerを設定し、評価シェーダーがgl_TessCoordを位置に変えます。ひび割れを避けるには、各辺のレベルを其の辺だけから決めます。 - コンピュートシェーダーはワークグループ(
local_size_*)で実行され、glDispatchComputeで切り上げて開始し、範囲を検査します。imageLoadとimageStoreで画像を読み書きし、セルが隣に依存するならピンポンのバッファが必要で、結果を使う前にglMemoryBarrierが必要です。 - コンピュートシェーダーにはOpenGL 4.3が必要でmacOSでは使えませんが、ジオメトリとテッセレーションのシェーダーは使えます。
15.10 課題
- ★ ライフゲームの格子を320 × 200にし、ワークグループを8 × 8にしました。
glDispatchComputeに何を渡し、何もしない呼び出しは幾つありますか(40 × 25グループで、何もしない呼び出しは0になるはずです)? - ★ 本文の
edgeLevelで、中点がカメラから4単位の辺は幾つに分割されますか?又、どの距離で辺は1つの区間まで落ちますか(24個、96単位以上)? - ★★ 法線のジオメトリシェーダーを拡張し、各三角形の面の法線も、三角形の中心から3番目の色で描いて下さい。
max_verticesを8に上げるのを忘れずに。 - ★★ ジオメトリシェーダーでワイヤーフレームを重ねて下さい。各三角形の3つの頂点に重心座標、、を渡し、フラグメントシェーダーで其の一番小さい値が0.02未満のピクセルを暗くします。陰影の付いた立方体と球の上に、全ての三角形の辺が暗い線として見えるはずです。
- ★★ 地形で
fractional_odd_spacingをequal_spacingに変え、カメラが動く間ワイヤーフレームを見て下さい。三角形が滑らかではなく、急に飛ぶ様に変わるはずです。 - ★★ ライフゲームの規則を「HighLife」(死んでいるセルは隣が6つでも生まれる)に変えて下さい。見た目は似ていますが、時々パターンが自分自身を複製するはずです。
- ★★★ コンピュートシェーダーのパーティクルシステムを書いて下さい。10 000個の位置と速度のバッファ(
layout(std430, binding = 0) buffer Particles { vec4 data[]; };)、重力を加えて床で跳ね返らせるコンピュートシェーダー、glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT)、そして同じバッファをGL_POINTSとして描きます。CPUに一度も触れない点の噴水が見えるはずです。
ファイル: a/main.c | b/main.c
| 前 | 新 | ||
|---|---|---|---|
| @@ -133,50 +133,85 @@ GLuint compileShader(GLenum type, const char *source) { | |||
| 133 | return shader; | 133 | return shader; |
| 134 | } | 134 | } |
| 135 | 135 | ||
| 136 | // 頂点シェーダーとフラグメントシェーダーのファイルからプログラムを作る(失敗したら0) | 136 | // シェーダーファイルを読み込んでコンパイルする(失敗したら0) |
| 137 | GLuint buildProgram(const char *vertexPath, const char *fragmentPath) { | 137 | GLuint compileShaderFile(GLenum type, const char *path) { |
| 138 | char *vertexSource = readShaderFile(vertexPath); | 138 | char *source = readShaderFile(path); |
| 139 | char *fragmentSource = readShaderFile(fragmentPath); | 139 | if (!source) return 0; |
| 140 | if (!vertexSource || !fragmentSource) { | ||
| 141 | free(vertexSource); | ||
| 142 | free(fragmentSource); | ||
| 143 | return 0; | ||
| 144 | } | ||
| 145 | 140 | ||
| 146 | #ifdef __APPLE__ | 141 | #ifdef __APPLE__ |
| 147 | // macOSはOpenGL 4.1までなので、「#version 450」を「#version 410」に書き換える | 142 | // macOSはOpenGL 4.1までなので、「#version 450」を「#version 410」に書き換える |
| 148 | char *v; | 143 | char *v; |
| 149 | if ((v = strstr(vertexSource, "#version 450"))) v[10] = '1'; | 144 | if ((v = strstr(source, "#version 450"))) v[10] = '1'; |
| 150 | if ((v = strstr(fragmentSource, "#version 450"))) v[10] = '1'; | ||
| 151 | #endif | 145 | #endif |
| 152 | 146 | ||
| 153 | GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexSource); | 147 | GLuint shader = compileShader(type, source); |
| 154 | GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentSource); | 148 | free(source); |
| 155 | free(vertexSource); | 149 | return shader; |
| 156 | free(fragmentSource); | 150 | } |
| 157 | if (!vertexShader || !fragmentShader) { | 151 | |
| 158 | if (vertexShader) glDeleteShader(vertexShader); | 152 | // シェーダーステージの種類(パスの配列の順番) |
| 159 | if (fragmentShader) glDeleteShader(fragmentShader); | 153 | enum { STAGE_VERTEX, STAGE_TESS_CONTROL, STAGE_TESS_EVALUATION, STAGE_GEOMETRY, STAGE_FRAGMENT, |
| 160 | return 0; | 154 | STAGE_COMPUTE, STAGE_COUNT }; |
| 155 | |||
| 156 | static const GLenum gStageTypes[STAGE_COUNT] = { | ||
| 157 | GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, | ||
| 158 | GL_FRAGMENT_SHADER, | ||
| 159 | #ifdef __APPLE__ | ||
| 160 | 0, // macOS(OpenGL 4.1)にはコンピュートシェーダーがない | ||
| 161 | #else | ||
| 162 | GL_COMPUTE_SHADER, | ||
| 163 | #endif | ||
| 164 | }; | ||
| 165 | |||
| 166 | // 好きなステージの組み合わせからプログラムを作る(NULLのステージは使わない、失敗したら0) | ||
| 167 | GLuint buildProgramStages(const char *const paths[STAGE_COUNT]) { | ||
| 168 | GLuint shaders[STAGE_COUNT] = { 0 }; | ||
| 169 | int ok = 1; | ||
| 170 | for (int i = 0; i < STAGE_COUNT; i++) { | ||
| 171 | if (!paths[i]) continue; | ||
| 172 | if (!gStageTypes[i]) { | ||
| 173 | printf("此のプラットフォームでは使えないシェーダーです: %s\n", paths[i]); | ||
| 174 | ok = 0; | ||
| 175 | break; | ||
| 176 | } | ||
| 177 | shaders[i] = compileShaderFile(gStageTypes[i], paths[i]); | ||
| 178 | if (!shaders[i]) { | ||
| 179 | ok = 0; | ||
| 180 | break; | ||
| 181 | } | ||
| 161 | } | 182 | } |
| 162 | 183 | ||
| 163 | GLuint program = glCreateProgram(); | 184 | GLuint program = 0; |
| 164 | glAttachShader(program, vertexShader); | 185 | if (ok) { |
| 165 | glAttachShader(program, fragmentShader); | 186 | program = glCreateProgram(); |
| 166 | glLinkProgram(program); | 187 | for (int i = 0; i < STAGE_COUNT; i++) { |
| 167 | glDeleteShader(vertexShader); | 188 | if (shaders[i]) glAttachShader(program, shaders[i]); |
| 168 | glDeleteShader(fragmentShader); | 189 | } |
| 190 | glLinkProgram(program); | ||
| 191 | |||
| 192 | GLint success; | ||
| 193 | glGetProgramiv(program, GL_LINK_STATUS, &success); | ||
| 194 | if (!success) { | ||
| 195 | checkProgramLink(program); | ||
| 196 | glDeleteProgram(program); | ||
| 197 | program = 0; | ||
| 198 | } | ||
| 199 | } | ||
| 169 | 200 | ||
| 170 | GLint success; | 201 | for (int i = 0; i < STAGE_COUNT; i++) { |
| 171 | glGetProgramiv(program, GL_LINK_STATUS, &success); | 202 | if (shaders[i]) glDeleteShader(shaders[i]); |
| 172 | if (!success) { | ||
| 173 | checkProgramLink(program); | ||
| 174 | glDeleteProgram(program); | ||
| 175 | return 0; | ||
| 176 | } | 203 | } |
| 177 | return program; | 204 | return program; |
| 178 | } | 205 | } |
| 179 | 206 | ||
| 207 | // 頂点シェーダーとフラグメントシェーダーのファイルからプログラムを作る(失敗したら0) | ||
| 208 | GLuint buildProgram(const char *vertexPath, const char *fragmentPath) { | ||
| 209 | const char *paths[STAGE_COUNT] = { NULL }; | ||
| 210 | paths[STAGE_VERTEX] = vertexPath; | ||
| 211 | paths[STAGE_FRAGMENT] = fragmentPath; | ||
| 212 | return buildProgramStages(paths); | ||
| 213 | } | ||
| 214 | |||
| 180 | #define PI 3.14159265358979f | 215 | #define PI 3.14159265358979f |
| 181 | 216 | ||
| 182 | // メッシュ(頂点配列オブジェクトとインデックスの数) | 217 | // メッシュ(頂点配列オブジェクトとインデックスの数) |
| @@ -332,6 +367,40 @@ Mesh createGrid(int nx, int ny, float w, float h) { | |||
| 332 | return mesh; | 367 | return mesh; |
| 333 | } | 368 | } |
| 334 | 369 | ||
| 370 | // XZ平面上のパッチの格子(一辺size、中心が原点):n × n個のパッチ、1パッチ=4頂点 | ||
| 371 | Mesh createPatchGrid(int n, float size) { | ||
| 372 | int vertexCount = (n + 1) * (n + 1); | ||
| 373 | int indexCount = n * n * 4; | ||
| 374 | float *vertices = malloc(vertexCount * VERTEX_FLOATS * sizeof(float)); | ||
| 375 | GLuint *indices = malloc(indexCount * sizeof(GLuint)); | ||
| 376 | |||
| 377 | float *out = vertices; | ||
| 378 | for (int j = 0; j <= n; j++) { | ||
| 379 | for (int i = 0; i <= n; i++) { | ||
| 380 | float u = (float)i / (float)n, v = (float)j / (float)n; | ||
| 381 | out[0] = (u - 0.5f) * size; out[1] = 0.0f; out[2] = (v - 0.5f) * size; | ||
| 382 | out[3] = 0.0f; out[4] = 1.0f; out[5] = 0.0f; | ||
| 383 | out[6] = u; out[7] = v; | ||
| 384 | out += VERTEX_FLOATS; | ||
| 385 | } | ||
| 386 | } | ||
| 387 | |||
| 388 | // 1パッチの頂点の順番:(u, v) = (0, 0)、(1, 0)、(1, 1)、(0, 1) | ||
| 389 | GLuint *idx = indices; | ||
| 390 | for (int j = 0; j < n; j++) { | ||
| 391 | for (int i = 0; i < n; i++) { | ||
| 392 | GLuint a = j * (n + 1) + i; | ||
| 393 | GLuint b = a + n + 1; | ||
| 394 | *idx++ = a; *idx++ = a + 1; *idx++ = b + 1; *idx++ = b; | ||
| 395 | } | ||
| 396 | } | ||
| 397 | |||
| 398 | Mesh mesh = createMesh(vertices, vertexCount, indices, indexCount); | ||
| 399 | free(vertices); | ||
| 400 | free(indices); | ||
| 401 | return mesh; | ||
| 402 | } | ||
| 403 | |||
| 335 | // 行列のユニフォームを設定(列優先のままなのでtransposeはGL_FALSE) | 404 | // 行列のユニフォームを設定(列優先のままなのでtransposeはGL_FALSE) |
| 336 | void setMat4(GLuint program, const char *name, Matrix4 m) { | 405 | void setMat4(GLuint program, const char *name, Matrix4 m) { |
| 337 | glUniformMatrix4fv(glGetUniformLocation(program, name), 1, GL_FALSE, m.m); | 406 | glUniformMatrix4fv(glGetUniformLocation(program, name), 1, GL_FALSE, m.m); |
| @@ -343,25 +412,42 @@ void setMat3(GLuint program, const char *name, Matrix3 m) { | |||
| 343 | 412 | ||
| 344 | // シェーダープログラムの一覧(Rキーで全部再読み込み) | 413 | // シェーダープログラムの一覧(Rキーで全部再読み込み) |
| 345 | typedef struct { | 414 | typedef struct { |
| 346 | const char *vertexPath; | 415 | const char *paths[STAGE_COUNT]; // 頂点、テッセレーション制御、テッセレーション評価、ジオメトリ、フラグメント、コンピュート |
| 347 | const char *fragmentPath; | ||
| 348 | GLuint id; | 416 | GLuint id; |
| 349 | } Program; | 417 | } Program; |
| 350 | 418 | ||
| 351 | enum { PROGRAM_SKY, PROGRAM_PHONG, PROGRAM_GOURAUD, PROGRAM_FLAG, PROGRAM_COUNT }; | 419 | enum { |
| 420 | PROGRAM_SKY, PROGRAM_PHONG, PROGRAM_GOURAUD, PROGRAM_FLAG, | ||
| 421 | PROGRAM_NORMALS, PROGRAM_EXPLODE, PROGRAM_TERRAIN, PROGRAM_LIFE_STEP, PROGRAM_LIFE_SHOW, | ||
| 422 | PROGRAM_COUNT | ||
| 423 | }; | ||
| 352 | 424 | ||
| 353 | static Program gPrograms[PROGRAM_COUNT] = { | 425 | static Program gPrograms[PROGRAM_COUNT] = { |
| 354 | { "shader/vertex_fullscreen.glsl", "shader/frag_sky.glsl", 0 }, | 426 | { { "shader/vertex_fullscreen.glsl", NULL, NULL, NULL, "shader/frag_sky.glsl", NULL }, 0 }, |
| 355 | { "shader/vertex_mesh.glsl", "shader/frag_phong.glsl", 0 }, | 427 | { { "shader/vertex_mesh.glsl", NULL, NULL, NULL, "shader/frag_phong.glsl", NULL }, 0 }, |
| 356 | { "shader/vertex_gouraud.glsl", "shader/frag_gouraud.glsl", 0 }, | 428 | { { "shader/vertex_gouraud.glsl", NULL, NULL, NULL, "shader/frag_gouraud.glsl", NULL }, 0 }, |
| 357 | { "shader/vertex_flag.glsl", "shader/frag_phong.glsl", 0 }, | 429 | { { "shader/vertex_flag.glsl", NULL, NULL, NULL, "shader/frag_phong.glsl", NULL }, 0 }, |
| 430 | // ジオメトリシェーダー:法線を線で描く、爆発 | ||
| 431 | { { "shader/vertex_passthrough.glsl", NULL, NULL, "shader/geom_normals.glsl", "shader/frag_line.glsl", | ||
| 432 | NULL }, 0 }, | ||
| 433 | { { "shader/vertex_passthrough.glsl", NULL, NULL, "shader/geom_explode.glsl", "shader/frag_phong.glsl", | ||
| 434 | NULL }, 0 }, | ||
| 435 | // テッセレーション:地形 | ||
| 436 | { { "shader/vertex_terrain.glsl", "shader/tesc_terrain.glsl", "shader/tese_terrain.glsl", NULL, | ||
| 437 | "shader/frag_terrain.glsl", NULL }, 0 }, | ||
| 438 | // コンピュートシェーダー:ライフゲーム(macOSでは使えない) | ||
| 439 | { { NULL, NULL, NULL, NULL, NULL, "shader/comp_life.glsl" }, 0 }, | ||
| 440 | { { "shader/vertex_fullscreen.glsl", NULL, NULL, NULL, "shader/frag_life.glsl", NULL }, 0 }, | ||
| 358 | }; | 441 | }; |
| 359 | 442 | ||
| 360 | // 全てのプログラムを作り直す(失敗した物は前のままにする)、失敗の数を返す | 443 | // 全てのプログラムを作り直す(失敗した物は前のままにする)、失敗の数を返す |
| 361 | int loadPrograms(void) { | 444 | int loadPrograms(void) { |
| 362 | int failed = 0; | 445 | int failed = 0; |
| 363 | for (int i = 0; i < PROGRAM_COUNT; i++) { | 446 | for (int i = 0; i < PROGRAM_COUNT; i++) { |
| 364 | GLuint program = buildProgram(gPrograms[i].vertexPath, gPrograms[i].fragmentPath); | 447 | #ifdef __APPLE__ |
| 448 | if (gPrograms[i].paths[STAGE_COMPUTE] || i == PROGRAM_LIFE_SHOW) continue; // デモ4はmacOSでは無し | ||
| 449 | #endif | ||
| 450 | GLuint program = buildProgramStages(gPrograms[i].paths); | ||
| 365 | if (program) { | 451 | if (program) { |
| 366 | if (gPrograms[i].id) glDeleteProgram(gPrograms[i].id); | 452 | if (gPrograms[i].id) glDeleteProgram(gPrograms[i].id); |
| 367 | gPrograms[i].id = program; | 453 | gPrograms[i].id = program; |
| @@ -391,6 +477,191 @@ void setCommonUniforms(GLuint program, Matrix4 view, Matrix4 proj, Vector3 eye, | |||
| 391 | glUniform1f(glGetUniformLocation(program, "uTime"), time); | 477 | glUniform1f(glGetUniformLocation(program, "uTime"), time); |
| 392 | } | 478 | } |
| 393 | 479 | ||
| 480 | // 全てのデモで使うメッシュ | ||
| 481 | static Mesh gCube, gSphere, gSmoothSphere, gFlag, gTerrain; | ||
| 482 | static GLuint gEmptyVAO; | ||
| 483 | |||
| 484 | // 背景:頂点バッファなしのフルスクリーン三角形(深度テストなし) | ||
| 485 | void drawSky(void) { | ||
| 486 | glDisable(GL_DEPTH_TEST); | ||
| 487 | GLuint sky = gPrograms[PROGRAM_SKY].id; | ||
| 488 | glUseProgram(sky); | ||
| 489 | glUniform2f(glGetUniformLocation(sky, "uResolution"), (float)gWidth, (float)gHeight); | ||
| 490 | glBindVertexArray(gEmptyVAO); | ||
| 491 | glDrawArrays(GL_TRIANGLES, 0, 3); | ||
| 492 | glEnable(GL_DEPTH_TEST); | ||
| 493 | } | ||
| 494 | |||
| 495 | Matrix4 cameraProjection(void) { | ||
| 496 | float aspect = gHeight > 0 ? (float)gWidth / (float)gHeight : 1.0f; | ||
| 497 | return mat4Perspective(50.0f * PI / 180.0f, aspect, 0.1f, 100.0f); | ||
| 498 | } | ||
| 499 | |||
| 500 | // デモ1:第13章のシーン | ||
| 501 | void drawSceneDemo(float time, int useGouraud) { | ||
| 502 | drawSky(); | ||
| 503 | |||
| 504 | // カメラ:ビュー行列と投影行列 | ||
| 505 | Vector3 eye = { { 0.0f, 1.0f, 5.0f } }; | ||
| 506 | Vector3 target = { { 0.0f, 0.1f, 0.0f } }; | ||
| 507 | Vector3 up = { { 0.0f, 1.0f, 0.0f } }; | ||
| 508 | Matrix4 view = mat4LookAt(eye, target, up); | ||
| 509 | Matrix4 proj = cameraProjection(); | ||
| 510 | |||
| 511 | // 立方体と球(Gキーでシェーダーを切り替え) | ||
| 512 | GLuint lit = useGouraud ? gPrograms[PROGRAM_GOURAUD].id : gPrograms[PROGRAM_PHONG].id; | ||
| 513 | setCommonUniforms(lit, view, proj, eye, time); | ||
| 514 | |||
| 515 | // 立方体:回転してから左へ移動(M = T * R) | ||
| 516 | Matrix4 cubeModel = mat4Multiply(mat4Translate(-1.7f, 0.1f, 0.0f), | ||
| 517 | mat4Multiply(mat4RotateY(time * 0.7f), mat4RotateX(0.6f))); | ||
| 518 | Vector3 cubeColor = { { 0.9f, 0.45f, 0.1f } }; | ||
| 519 | drawLit(lit, &gCube, cubeModel, cubeColor, 1); | ||
| 520 | |||
| 521 | // 球:わざと潰してから回転(M = T * R * S)、法線行列が必要になる | ||
| 522 | Matrix4 sphereModel = mat4Multiply(mat4Translate(0.0f, 0.1f, 0.0f), | ||
| 523 | mat4Multiply(mat4RotateY(time * 0.5f), | ||
| 524 | mat4Scale(0.9f, 0.6f, 0.9f))); | ||
| 525 | Vector3 sphereColor = { { 0.2f, 0.45f, 0.9f } }; | ||
| 526 | drawLit(lit, &gSphere, sphereModel, sphereColor, 1); | ||
| 527 | |||
| 528 | // 旗竿:細長く伸ばした立方体 | ||
| 529 | Matrix4 poleModel = mat4Multiply(mat4Translate(1.2f, 0.0f, 0.0f), mat4Scale(0.05f, 2.2f, 0.05f)); | ||
| 530 | Vector3 poleColor = { { 0.8f, 0.8f, 0.75f } }; | ||
| 531 | drawLit(lit, &gCube, poleModel, poleColor, 0); | ||
| 532 | |||
| 533 | // 旗:頂点シェーダーで波打たせる(両面を描くのでカリングなし) | ||
| 534 | GLuint flagProgram = gPrograms[PROGRAM_FLAG].id; | ||
| 535 | setCommonUniforms(flagProgram, view, proj, eye, time); | ||
| 536 | glDisable(GL_CULL_FACE); | ||
| 537 | Vector3 flagColor = { { 0.95f, 0.95f, 0.92f } }; | ||
| 538 | Matrix4 flagModel = mat4Multiply(mat4Translate(1.2f, 0.2f, 0.0f), mat4RotateY(0.6f)); | ||
| 539 | drawLit(flagProgram, &gFlag, flagModel, flagColor, 2); | ||
| 540 | glEnable(GL_CULL_FACE); | ||
| 541 | } | ||
| 542 | |||
| 543 | // デモ2:ジオメトリシェーダー(左:爆発する球、右:法線を線で描いた球) | ||
| 544 | void drawGeometryDemo(float time) { | ||
| 545 | drawSky(); | ||
| 546 | |||
| 547 | Vector3 eye = { { 0.0f, 1.0f, 5.0f } }; | ||
| 548 | Vector3 target = { { 0.0f, 0.1f, 0.0f } }; | ||
| 549 | Vector3 up = { { 0.0f, 1.0f, 0.0f } }; | ||
| 550 | Matrix4 view = mat4LookAt(eye, target, up); | ||
| 551 | Matrix4 proj = cameraProjection(); | ||
| 552 | |||
| 553 | // 爆発:三角形を面の法線の方向へ押し出す(裏側も見えるのでカリングなし) | ||
| 554 | GLuint explode = gPrograms[PROGRAM_EXPLODE].id; | ||
| 555 | setCommonUniforms(explode, view, proj, eye, time); | ||
| 556 | float amount = 0.5f * (0.5f - 0.5f * cosf(time * 1.2f)); // 0〜0.5を行ったり来たり | ||
| 557 | glUniform1f(glGetUniformLocation(explode, "uExplode"), amount); | ||
| 558 | Matrix4 leftModel = mat4Multiply(mat4Translate(-1.3f, 0.1f, 0.0f), | ||
| 559 | mat4Multiply(mat4RotateY(time * 0.4f), mat4Scale(0.8f, 0.8f, 0.8f))); | ||
| 560 | Vector3 leftColor = { { 0.9f, 0.3f, 0.35f } }; | ||
| 561 | glDisable(GL_CULL_FACE); | ||
| 562 | drawLit(explode, &gSmoothSphere, leftModel, leftColor, 1); | ||
| 563 | glEnable(GL_CULL_FACE); | ||
| 564 | |||
| 565 | // 右:普通に描いてから、同じメッシュの法線を線で重ねる | ||
| 566 | Matrix4 rightModel = mat4Multiply(mat4Translate(1.3f, 0.1f, 0.0f), | ||
| 567 | mat4Multiply(mat4RotateY(time * 0.4f), mat4Scale(0.9f, 0.6f, 0.9f))); | ||
| 568 | GLuint lit = gPrograms[PROGRAM_PHONG].id; | ||
| 569 | setCommonUniforms(lit, view, proj, eye, time); | ||
| 570 | Vector3 rightColor = { { 0.2f, 0.45f, 0.9f } }; | ||
| 571 | drawLit(lit, &gSphere, rightModel, rightColor, 0); | ||
| 572 | |||
| 573 | GLuint normals = gPrograms[PROGRAM_NORMALS].id; | ||
| 574 | setCommonUniforms(normals, view, proj, eye, time); | ||
| 575 | glUniform1f(glGetUniformLocation(normals, "uLength"), 0.2f); | ||
| 576 | drawLit(normals, &gSphere, rightModel, rightColor, 0); | ||
| 577 | } | ||
| 578 | |||
| 579 | // デモ3:テッセレーションで地形を細かくする | ||
| 580 | void drawTessellationDemo(float time, int wireframe) { | ||
| 581 | drawSky(); | ||
| 582 | |||
| 583 | // カメラ:地形の周りをゆっくり回る | ||
| 584 | float angle = time * 0.15f; | ||
| 585 | Vector3 eye = { { 8.0f * sinf(angle), 6.0f, 8.0f * cosf(angle) } }; | ||
| 586 | Vector3 target = { { 0.0f, 0.0f, 0.0f } }; | ||
| 587 | Vector3 up = { { 0.0f, 1.0f, 0.0f } }; | ||
| 588 | Matrix4 view = mat4LookAt(eye, target, up); | ||
| 589 | Matrix4 proj = cameraProjection(); | ||
| 590 | |||
| 591 | GLuint terrain = gPrograms[PROGRAM_TERRAIN].id; | ||
| 592 | setCommonUniforms(terrain, view, proj, eye, time); | ||
| 593 | |||
| 594 | if (wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); | ||
| 595 | glDisable(GL_CULL_FACE); | ||
| 596 | glPatchParameteri(GL_PATCH_VERTICES, 4); // 1パッチ=4頂点 | ||
| 597 | glBindVertexArray(gTerrain.vao); | ||
| 598 | glDrawElements(GL_PATCHES, gTerrain.indexCount, GL_UNSIGNED_INT, 0); | ||
| 599 | glEnable(GL_CULL_FACE); | ||
| 600 | glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); | ||
| 601 | } | ||
| 602 | |||
| 603 | #ifndef __APPLE__ | ||
| 604 | // デモ4:コンピュートシェーダーのライフゲーム(OpenGL 4.3以上) | ||
| 605 | #define LIFE_WIDTH 200 | ||
| 606 | #define LIFE_HEIGHT 150 | ||
| 607 | |||
| 608 | static GLuint gLifeTextures[2]; | ||
| 609 | static int gLifeCurrent = 0; | ||
| 610 | |||
| 611 | // セルをランダムに置く(r=生きているか、g=残像) | ||
| 612 | void resetLife(unsigned int seed) { | ||
| 613 | static unsigned char cells[LIFE_WIDTH * LIFE_HEIGHT * 4]; | ||
| 614 | for (int i = 0; i < LIFE_WIDTH * LIFE_HEIGHT; i++) { | ||
| 615 | seed = seed * 1664525u + 1013904223u; // 線型合同法 | ||
| 616 | unsigned char alive = (seed >> 24) < 64 ? 255 : 0; // 約25% | ||
| 617 | cells[i * 4 + 0] = alive; | ||
| 618 | cells[i * 4 + 1] = alive; | ||
| 619 | cells[i * 4 + 2] = 0; | ||
| 620 | cells[i * 4 + 3] = 255; | ||
| 621 | } | ||
| 622 | glBindTexture(GL_TEXTURE_2D, gLifeTextures[gLifeCurrent]); | ||
| 623 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LIFE_WIDTH, LIFE_HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, cells); | ||
| 624 | } | ||
| 625 | |||
| 626 | void createLife(void) { | ||
| 627 | glGenTextures(2, gLifeTextures); | ||
| 628 | for (int i = 0; i < 2; i++) { | ||
| 629 | glBindTexture(GL_TEXTURE_2D, gLifeTextures[i]); | ||
| 630 | glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, LIFE_WIDTH, LIFE_HEIGHT); // 大きさと形式を固定 | ||
| 631 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); | ||
| 632 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); | ||
| 633 | } | ||
| 634 | resetLife(76); | ||
| 635 | } | ||
| 636 | |||
| 637 | // 1世代進める:今のテクスチャを読み、もう一方に書く | ||
| 638 | void stepLife(void) { | ||
| 639 | GLuint program = gPrograms[PROGRAM_LIFE_STEP].id; | ||
| 640 | glUseProgram(program); | ||
| 641 | glBindImageTexture(0, gLifeTextures[gLifeCurrent], 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); | ||
| 642 | glBindImageTexture(1, gLifeTextures[1 - gLifeCurrent], 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); | ||
| 643 | |||
| 644 | // ワークグループは16 × 16なので、切り上げて画像全体を覆う | ||
| 645 | glDispatchCompute((LIFE_WIDTH + 15) / 16, (LIFE_HEIGHT + 15) / 16, 1); | ||
| 646 | |||
| 647 | // 書き込みが終わってから次の読み込み(imageLoadとtexture)をする | ||
| 648 | glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT); | ||
| 649 | gLifeCurrent = 1 - gLifeCurrent; | ||
| 650 | } | ||
| 651 | |||
| 652 | void drawComputeDemo(void) { | ||
| 653 | glDisable(GL_DEPTH_TEST); | ||
| 654 | GLuint show = gPrograms[PROGRAM_LIFE_SHOW].id; | ||
| 655 | glUseProgram(show); | ||
| 656 | glActiveTexture(GL_TEXTURE0); | ||
| 657 | glBindTexture(GL_TEXTURE_2D, gLifeTextures[gLifeCurrent]); | ||
| 658 | glUniform1i(glGetUniformLocation(show, "uLife"), 0); | ||
| 659 | glBindVertexArray(gEmptyVAO); | ||
| 660 | glDrawArrays(GL_TRIANGLES, 0, 3); | ||
| 661 | glEnable(GL_DEPTH_TEST); | ||
| 662 | } | ||
| 663 | #endif | ||
| 664 | |||
| 394 | int main(int argc, char **argv) { | 665 | int main(int argc, char **argv) { |
| 395 | (void)argc; | 666 | (void)argc; |
| 396 | (void)argv; | 667 | (void)argv; |
| @@ -417,7 +688,7 @@ int main(int argc, char **argv) { | |||
| 417 | #endif | 688 | #endif |
| 418 | 689 | ||
| 419 | // ウィンドウを作成 | 690 | // ウィンドウを作成 |
| 420 | GLFWwindow *window = glfwCreateWindow(800, 600, "GLSLシェーダー:頂点シェーダーと3D", NULL, NULL); | 691 | GLFWwindow *window = glfwCreateWindow(800, 600, "1:3Dシーン", NULL, NULL); |
| 421 | if (!window) { | 692 | if (!window) { |
| 422 | puts("GLFWウィンドウの作成に失敗しました"); | 693 | puts("GLFWウィンドウの作成に失敗しました"); |
| 423 | glfwTerminate(); | 694 | glfwTerminate(); |
| @@ -447,13 +718,18 @@ int main(int argc, char **argv) { | |||
| 447 | } | 718 | } |
| 448 | 719 | ||
| 449 | // メッシュを作る | 720 | // メッシュを作る |
| 450 | Mesh cube = createCube(); | 721 | gCube = createCube(); |
| 451 | Mesh sphere = createSphere(16, 12); | 722 | gSphere = createSphere(16, 12); |
| 452 | Mesh flag = createGrid(32, 20, 1.4f, 0.9f); | 723 | gSmoothSphere = createSphere(32, 24); |
| 724 | gFlag = createGrid(32, 20, 1.4f, 0.9f); | ||
| 725 | gTerrain = createPatchGrid(8, 10.0f); | ||
| 453 | 726 | ||
| 454 | // 頂点バッファなしで描く為の空のVAO(コアプロファイルではVAOが必須) | 727 | // 頂点バッファなしで描く為の空のVAO(コアプロファイルではVAOが必須) |
| 455 | GLuint emptyVAO; | 728 | glGenVertexArrays(1, &gEmptyVAO); |
| 456 | glGenVertexArrays(1, &emptyVAO); | 729 | |
| 730 | #ifndef __APPLE__ | ||
| 731 | createLife(); | ||
| 732 | #endif | ||
| 457 | 733 | ||
| 458 | // 深度テストと裏面カリング | 734 | // 深度テストと裏面カリング |
| 459 | glEnable(GL_DEPTH_TEST); | 735 | glEnable(GL_DEPTH_TEST); |
| @@ -461,15 +737,31 @@ int main(int argc, char **argv) { | |||
| 461 | glCullFace(GL_BACK); | 737 | glCullFace(GL_BACK); |
| 462 | glFrontFace(GL_CCW); | 738 | glFrontFace(GL_CCW); |
| 463 | 739 | ||
| 464 | int reloadWasDown = 0; | 740 | const char *demoNames[4] = { |
| 465 | int gouraudWasDown = 0; | 741 | "1:3Dシーン", "2:ジオメトリシェーダー", "3:テッセレーション", "4:コンピュートシェーダー" |
| 466 | int useGouraud = 0; | 742 | }; |
| 743 | int demo = 0; | ||
| 744 | int reloadWasDown = 0, gouraudWasDown = 0, wireWasDown = 0, spaceWasDown = 0; | ||
| 745 | int useGouraud = 0, wireframe = 0; | ||
| 746 | float lastLifeStep = 0.0f; | ||
| 467 | 747 | ||
| 468 | // メインレンダリングループ | 748 | // メインレンダリングループ |
| 469 | while (!glfwWindowShouldClose(window)) { | 749 | while (!glfwWindowShouldClose(window)) { |
| 470 | // ESCキーで終了 | 750 | // ESCキーで終了 |
| 471 | if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); | 751 | if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); |
| 472 | 752 | ||
| 753 | // 1〜4キーでデモを切り替える | ||
| 754 | for (int i = 0; i < 4; i++) { | ||
| 755 | if (glfwGetKey(window, GLFW_KEY_1 + i) == GLFW_PRESS && demo != i) { | ||
| 756 | #ifdef __APPLE__ | ||
| 757 | if (i == 3) continue; // コンピュートシェーダーはmacOSでは使えない | ||
| 758 | #endif | ||
| 759 | demo = i; | ||
| 760 | glfwSetWindowTitle(window, demoNames[i]); | ||
| 761 | puts(demoNames[i]); | ||
| 762 | } | ||
| 763 | } | ||
| 764 | |||
| 473 | // Rキーでシェーダーを再読み込み(失敗したら古いシェーダーのまま) | 765 | // Rキーでシェーダーを再読み込み(失敗したら古いシェーダーのまま) |
| 474 | int reloadDown = glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS; | 766 | int reloadDown = glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS; |
| 475 | if (reloadDown && !reloadWasDown) { | 767 | if (reloadDown && !reloadWasDown) { |
| @@ -486,59 +778,33 @@ int main(int argc, char **argv) { | |||
| 486 | } | 778 | } |
| 487 | gouraudWasDown = gouraudDown; | 779 | gouraudWasDown = gouraudDown; |
| 488 | 780 | ||
| 781 | // Wキーでワイヤーフレーム(デモ3) | ||
| 782 | int wireDown = glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS; | ||
| 783 | if (wireDown && !wireWasDown) wireframe = !wireframe; | ||
| 784 | wireWasDown = wireDown; | ||
| 785 | |||
| 489 | float time = (float)glfwGetTime(); | 786 | float time = (float)glfwGetTime(); |
| 490 | 787 | ||
| 491 | // 画面と深度バッファをクリア | 788 | // 画面と深度バッファをクリア |
| 492 | glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | 789 | glClearColor(0.0f, 0.0f, 0.0f, 1.0f); |
| 493 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); | 790 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); |
| 494 | 791 | ||
| 495 | // 背景:頂点バッファなしのフルスクリーン三角形(深度テストなし) | 792 | if (demo == 0) drawSceneDemo(time, useGouraud); |
| 496 | glDisable(GL_DEPTH_TEST); | 793 | else if (demo == 1) drawGeometryDemo(time); |
| 497 | GLuint sky = gPrograms[PROGRAM_SKY].id; | 794 | else if (demo == 2) drawTessellationDemo(time, wireframe); |
| 498 | glUseProgram(sky); | 795 | #ifndef __APPLE__ |
| 499 | glUniform2f(glGetUniformLocation(sky, "uResolution"), (float)gWidth, (float)gHeight); | 796 | else { |
| 500 | glBindVertexArray(emptyVAO); | 797 | // スペースキーでやり直し、1秒に20世代進める |
| 501 | glDrawArrays(GL_TRIANGLES, 0, 3); | 798 | int spaceDown = glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS; |
| 502 | glEnable(GL_DEPTH_TEST); | 799 | if (spaceDown && !spaceWasDown) resetLife((unsigned int)(time * 1000.0f)); |
| 503 | 800 | spaceWasDown = spaceDown; | |
| 504 | // カメラ:ビュー行列と投影行列 | 801 | if (time - lastLifeStep >= 0.05f) { |
| 505 | Vector3 eye = { { 0.0f, 1.0f, 5.0f } }; | 802 | stepLife(); |
| 506 | Vector3 target = { { 0.0f, 0.1f, 0.0f } }; | 803 | lastLifeStep = time; |
| 507 | Vector3 up = { { 0.0f, 1.0f, 0.0f } }; | 804 | } |
| 508 | Matrix4 view = mat4LookAt(eye, target, up); | 805 | drawComputeDemo(); |
| 509 | float aspect = gHeight > 0 ? (float)gWidth / (float)gHeight : 1.0f; | 806 | } |
| 510 | Matrix4 proj = mat4Perspective(50.0f * PI / 180.0f, aspect, 0.1f, 100.0f); | 807 | #endif |
| 511 | |||
| 512 | // 立方体と球(Gキーでシェーダーを切り替え) | ||
| 513 | GLuint lit = useGouraud ? gPrograms[PROGRAM_GOURAUD].id : gPrograms[PROGRAM_PHONG].id; | ||
| 514 | setCommonUniforms(lit, view, proj, eye, time); | ||
| 515 | |||
| 516 | // 立方体:回転してから左へ移動(M = T * R) | ||
| 517 | Matrix4 cubeModel = mat4Multiply(mat4Translate(-1.7f, 0.1f, 0.0f), | ||
| 518 | mat4Multiply(mat4RotateY(time * 0.7f), mat4RotateX(0.6f))); | ||
| 519 | Vector3 cubeColor = { { 0.9f, 0.45f, 0.1f } }; | ||
| 520 | drawLit(lit, &cube, cubeModel, cubeColor, 1); | ||
| 521 | |||
| 522 | // 球:わざと潰してから回転(M = T * R * S)、法線行列が必要になる | ||
| 523 | Matrix4 sphereModel = mat4Multiply(mat4Translate(0.0f, 0.1f, 0.0f), | ||
| 524 | mat4Multiply(mat4RotateY(time * 0.5f), | ||
| 525 | mat4Scale(0.9f, 0.6f, 0.9f))); | ||
| 526 | Vector3 sphereColor = { { 0.2f, 0.45f, 0.9f } }; | ||
| 527 | drawLit(lit, &sphere, sphereModel, sphereColor, 1); | ||
| 528 | |||
| 529 | // 旗竿:細長く伸ばした立方体 | ||
| 530 | Matrix4 poleModel = mat4Multiply(mat4Translate(1.2f, 0.0f, 0.0f), mat4Scale(0.05f, 2.2f, 0.05f)); | ||
| 531 | Vector3 poleColor = { { 0.8f, 0.8f, 0.75f } }; | ||
| 532 | drawLit(lit, &cube, poleModel, poleColor, 0); | ||
| 533 | |||
| 534 | // 旗:頂点シェーダーで波打たせる(両面を描くのでカリングなし) | ||
| 535 | GLuint flagProgram = gPrograms[PROGRAM_FLAG].id; | ||
| 536 | setCommonUniforms(flagProgram, view, proj, eye, time); | ||
| 537 | glDisable(GL_CULL_FACE); | ||
| 538 | Vector3 flagColor = { { 0.95f, 0.95f, 0.92f } }; | ||
| 539 | Matrix4 flagModel = mat4Multiply(mat4Translate(1.2f, 0.2f, 0.0f), mat4RotateY(0.6f)); | ||
| 540 | drawLit(flagProgram, &flag, flagModel, flagColor, 2); | ||
| 541 | glEnable(GL_CULL_FACE); | ||
| 542 | 808 | ||
| 543 | // バッファをスワップし、イベントを処理 | 809 | // バッファをスワップし、イベントを処理 |
| 544 | glfwSwapBuffers(window); | 810 | glfwSwapBuffers(window); |
| @@ -546,10 +812,15 @@ int main(int argc, char **argv) { | |||
| 546 | } | 812 | } |
| 547 | 813 | ||
| 548 | // クリーンアップ | 814 | // クリーンアップ |
| 549 | destroyMesh(&cube); | 815 | destroyMesh(&gCube); |
| 550 | destroyMesh(&sphere); | 816 | destroyMesh(&gSphere); |
| 551 | destroyMesh(&flag); | 817 | destroyMesh(&gSmoothSphere); |
| 552 | glDeleteVertexArrays(1, &emptyVAO); | 818 | destroyMesh(&gFlag); |
| 819 | destroyMesh(&gTerrain); | ||
| 820 | glDeleteVertexArrays(1, &gEmptyVAO); | ||
| 821 | #ifndef __APPLE__ | ||
| 822 | glDeleteTextures(2, gLifeTextures); | ||
| 823 | #endif | ||
| 553 | for (int i = 0; i < PROGRAM_COUNT; i++) glDeleteProgram(gPrograms[i].id); | 824 | for (int i = 0; i < PROGRAM_COUNT; i++) glDeleteProgram(gPrograms[i].id); |
| 554 | glfwDestroyWindow(window); | 825 | glfwDestroyWindow(window); |
| 555 | glfwTerminate(); | 826 | glfwTerminate(); |
ファイル: /dev/null | b/shader/comp_life.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,32 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | // 1つのワークグループ=16 × 16個の呼び出し | ||
| 4 | layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; | ||
| 5 | |||
| 6 | layout(rgba8, binding = 0) uniform readonly image2D uCurrent; // 今の世代 | ||
| 7 | layout(rgba8, binding = 1) uniform writeonly image2D uNext; // 次の世代 | ||
| 8 | |||
| 9 | void main() { | ||
| 10 | ivec2 size = imageSize(uCurrent); | ||
| 11 | ivec2 p = ivec2(gl_GlobalInvocationID.xy); // 此の呼び出しが担当するセル | ||
| 12 | if (p.x >= size.x || p.y >= size.y) return; // 端のワークグループのはみ出し | ||
| 13 | |||
| 14 | // 周りの8つのセルを数える(端は反対側に繋がる) | ||
| 15 | int neighbours = 0; | ||
| 16 | for (int dy = -1; dy <= 1; dy++) { | ||
| 17 | for (int dx = -1; dx <= 1; dx++) { | ||
| 18 | if (dx == 0 && dy == 0) continue; | ||
| 19 | ivec2 q = (p + ivec2(dx, dy) + size) % size; | ||
| 20 | if (imageLoad(uCurrent, q).r > 0.5) neighbours++; | ||
| 21 | } | ||
| 22 | } | ||
| 23 | |||
| 24 | // ライフゲームの規則:生きているセルは2か3で生き残り、死んでいるセルは3で生まれる | ||
| 25 | vec4 cell = imageLoad(uCurrent, p); | ||
| 26 | bool alive = cell.r > 0.5; | ||
| 27 | bool next = alive ? (neighbours == 2 || neighbours == 3) : (neighbours == 3); | ||
| 28 | |||
| 29 | // g:残像(死んだ後ゆっくり消える) | ||
| 30 | float trail = next ? 1.0 : cell.g * 0.92; | ||
| 31 | imageStore(uNext, p, vec4(next ? 1.0 : 0.0, trail, 0.0, 1.0)); | ||
| 32 | } | ||
ファイル: /dev/null | b/shader/frag_life.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,13 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | in vec2 vUv; | ||
| 4 | out vec4 out_color; | ||
| 5 | |||
| 6 | uniform sampler2D uLife; // コンピュートシェーダーが書いたテクスチャ | ||
| 7 | |||
| 8 | void main() { | ||
| 9 | vec4 cell = texture(uLife, vUv); | ||
| 10 | vec3 trail = vec3(0.15, 0.1, 0.45) * cell.g; // 残像:紫 | ||
| 11 | vec3 alive = vec3(1.0, 0.9, 0.5) * cell.r; // 生きているセル:明るい黄色 | ||
| 12 | out_color = vec4(max(trail, alive), 1.0); | ||
| 13 | } | ||
ファイル: /dev/null | b/shader/frag_line.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,8 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | in vec3 gColor; | ||
| 4 | out vec4 out_color; | ||
| 5 | |||
| 6 | void main() { | ||
| 7 | out_color = vec4(gColor, 1.0); | ||
| 8 | } | ||
ファイル: /dev/null | b/shader/frag_terrain.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,32 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | in vec3 vWorldPos; | ||
| 4 | in vec3 vNormal; | ||
| 5 | out vec4 out_color; | ||
| 6 | |||
| 7 | uniform vec3 uLightDir; | ||
| 8 | uniform vec3 uCameraPos; | ||
| 9 | |||
| 10 | void main() { | ||
| 11 | vec3 n = normalize(vNormal); | ||
| 12 | float h = vWorldPos.y; | ||
| 13 | |||
| 14 | // 高さと傾きで色を決める | ||
| 15 | vec3 rock = vec3(0.3, 0.27, 0.25); | ||
| 16 | vec3 color = vec3(0.05, 0.15, 0.35); // 水 | ||
| 17 | color = mix(color, vec3(0.6, 0.55, 0.35), smoothstep(0.0, 0.05, h)); // 砂 | ||
| 18 | color = mix(color, vec3(0.12, 0.35, 0.08), smoothstep(0.1, 0.3, h)); // 草 | ||
| 19 | color = mix(color, rock, smoothstep(0.55, 0.8, h)); // 岩 | ||
| 20 | color = mix(color, rock, 1.0 - smoothstep(0.6, 0.75, n.y)); // 急な斜面も岩 | ||
| 21 | color = mix(color, vec3(0.9), smoothstep(1.1, 1.3, h)); // 雪 | ||
| 22 | |||
| 23 | float diffuse = max(dot(n, normalize(uLightDir)), 0.0); | ||
| 24 | vec3 lit = color * (0.15 + diffuse); | ||
| 25 | |||
| 26 | // 遠い程空の色に | ||
| 27 | float d = distance(vWorldPos, uCameraPos); | ||
| 28 | float fog = 1.0 - exp(-0.002 * d * d); | ||
| 29 | lit = mix(lit, vec3(0.32, 0.30, 0.36), fog); | ||
| 30 | |||
| 31 | out_color = vec4(pow(lit, vec3(1.0 / 2.2)), 1.0); | ||
| 32 | } | ||
ファイル: /dev/null | b/shader/geom_explode.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,38 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | layout(triangles) in; | ||
| 4 | layout(triangle_strip, max_vertices = 3) out; | ||
| 5 | |||
| 6 | in vec3 vsPosition[]; | ||
| 7 | in vec3 vsNormal[]; | ||
| 8 | in vec2 vsUv[]; | ||
| 9 | |||
| 10 | // フラグメントシェーダー(frag_phong.glsl)が受け取る名前 | ||
| 11 | out vec3 vWorldPos; | ||
| 12 | out vec3 vNormal; | ||
| 13 | out vec2 vUv; | ||
| 14 | |||
| 15 | uniform mat4 uModel; | ||
| 16 | uniform mat4 uView; | ||
| 17 | uniform mat4 uProj; | ||
| 18 | uniform mat3 uNormalMatrix; | ||
| 19 | uniform float uExplode; // 押し出す距離 | ||
| 20 | |||
| 21 | void main() { | ||
| 22 | vec3 world[3]; | ||
| 23 | for (int i = 0; i < 3; i++) world[i] = (uModel * vec4(vsPosition[i], 1.0)).xyz; | ||
| 24 | |||
| 25 | // 面の法線:二つの辺の外積(線型代数の第5章) | ||
| 26 | vec3 faceNormal = normalize(cross(world[1] - world[0], world[2] - world[0])); | ||
| 27 | vec3 offset = faceNormal * uExplode; | ||
| 28 | |||
| 29 | mat4 viewProj = uProj * uView; | ||
| 30 | for (int i = 0; i < 3; i++) { | ||
| 31 | vWorldPos = world[i] + offset; | ||
| 32 | vNormal = uNormalMatrix * vsNormal[i]; | ||
| 33 | vUv = vsUv[i]; | ||
| 34 | gl_Position = viewProj * vec4(vWorldPos, 1.0); | ||
| 35 | EmitVertex(); | ||
| 36 | } | ||
| 37 | EndPrimitive(); | ||
| 38 | } | ||
ファイル: /dev/null | b/shader/geom_normals.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,33 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | layout(triangles) in; // 入力:三角形(頂点3つ) | ||
| 4 | layout(line_strip, max_vertices = 6) out; // 出力:線(頂点2つ × 3本) | ||
| 5 | |||
| 6 | in vec3 vsPosition[]; // 頂点シェーダーの出力は配列になる | ||
| 7 | in vec3 vsNormal[]; | ||
| 8 | |||
| 9 | out vec3 gColor; | ||
| 10 | |||
| 11 | uniform mat4 uModel; | ||
| 12 | uniform mat4 uView; | ||
| 13 | uniform mat4 uProj; | ||
| 14 | uniform mat3 uNormalMatrix; | ||
| 15 | uniform float uLength; // 線の長さ(ワールド空間) | ||
| 16 | |||
| 17 | void main() { | ||
| 18 | mat4 viewProj = uProj * uView; | ||
| 19 | for (int i = 0; i < 3; i++) { | ||
| 20 | vec3 p = (uModel * vec4(vsPosition[i], 1.0)).xyz; | ||
| 21 | vec3 n = normalize(uNormalMatrix * vsNormal[i]); | ||
| 22 | |||
| 23 | gColor = vec3(1.0, 0.9, 0.2); // 根元:黄色 | ||
| 24 | gl_Position = viewProj * vec4(p, 1.0); | ||
| 25 | EmitVertex(); | ||
| 26 | |||
| 27 | gColor = vec3(1.0, 0.2, 0.1); // 先:赤 | ||
| 28 | gl_Position = viewProj * vec4(p + n * uLength, 1.0); | ||
| 29 | EmitVertex(); | ||
| 30 | |||
| 31 | EndPrimitive(); // 1本の線が終わり | ||
| 32 | } | ||
| 33 | } | ||
ファイル: /dev/null | b/shader/tesc_terrain.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,30 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | layout(vertices = 4) out; // 出力パッチの制御点の数 | ||
| 4 | |||
| 5 | in vec3 vsPosition[]; | ||
| 6 | out vec3 tcPosition[]; | ||
| 7 | |||
| 8 | uniform vec3 uCameraPos; | ||
| 9 | |||
| 10 | // 辺の中点がカメラに近い程細かくする | ||
| 11 | float edgeLevel(vec3 a, vec3 b) { | ||
| 12 | float d = distance(0.5 * (a + b), uCameraPos); | ||
| 13 | return clamp(96.0 / d, 1.0, 64.0); | ||
| 14 | } | ||
| 15 | |||
| 16 | void main() { | ||
| 17 | // 制御点はそのまま渡す(gl_InvocationIDは0〜3) | ||
| 18 | tcPosition[gl_InvocationID] = vsPosition[gl_InvocationID]; | ||
| 19 | |||
| 20 | // 分割数はパッチに1回だけ決めれば良い | ||
| 21 | if (gl_InvocationID == 0) { | ||
| 22 | // 角の順番:0 = (0, 0)、1 = (1, 0)、2 = (1, 1)、3 = (0, 1) | ||
| 23 | gl_TessLevelOuter[0] = edgeLevel(vsPosition[3], vsPosition[0]); // u = 0の辺 | ||
| 24 | gl_TessLevelOuter[1] = edgeLevel(vsPosition[0], vsPosition[1]); // v = 0の辺 | ||
| 25 | gl_TessLevelOuter[2] = edgeLevel(vsPosition[1], vsPosition[2]); // u = 1の辺 | ||
| 26 | gl_TessLevelOuter[3] = edgeLevel(vsPosition[2], vsPosition[3]); // v = 1の辺 | ||
| 27 | gl_TessLevelInner[0] = max(gl_TessLevelOuter[1], gl_TessLevelOuter[3]); // u方向 | ||
| 28 | gl_TessLevelInner[1] = max(gl_TessLevelOuter[0], gl_TessLevelOuter[2]); // v方向 | ||
| 29 | } | ||
| 30 | } | ||
ファイル: /dev/null | b/shader/tese_terrain.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,65 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | layout(quads, fractional_odd_spacing, ccw) in; | ||
| 4 | |||
| 5 | in vec3 tcPosition[]; | ||
| 6 | |||
| 7 | out vec3 vWorldPos; | ||
| 8 | out vec3 vNormal; | ||
| 9 | |||
| 10 | uniform mat4 uView; | ||
| 11 | uniform mat4 uProj; | ||
| 12 | |||
| 13 | // 第8章の様なバリューノイズとfBm | ||
| 14 | float hash(vec2 p) { | ||
| 15 | p = fract(p * vec2(123.34, 456.21)); | ||
| 16 | p += dot(p, p + 45.32); | ||
| 17 | return fract(p.x * p.y); | ||
| 18 | } | ||
| 19 | |||
| 20 | float valueNoise(vec2 p) { | ||
| 21 | vec2 i = floor(p); | ||
| 22 | vec2 f = fract(p); | ||
| 23 | vec2 u = f * f * (3.0 - 2.0 * f); | ||
| 24 | float a = hash(i); | ||
| 25 | float b = hash(i + vec2(1.0, 0.0)); | ||
| 26 | float c = hash(i + vec2(0.0, 1.0)); | ||
| 27 | float d = hash(i + vec2(1.0, 1.0)); | ||
| 28 | return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); | ||
| 29 | } | ||
| 30 | |||
| 31 | float fbm(vec2 p) { | ||
| 32 | float sum = 0.0; | ||
| 33 | float amplitude = 0.5; | ||
| 34 | for (int i = 0; i < 4; i++) { | ||
| 35 | sum += amplitude * valueNoise(p); | ||
| 36 | p *= 2.0; | ||
| 37 | amplitude *= 0.5; | ||
| 38 | } | ||
| 39 | return sum; | ||
| 40 | } | ||
| 41 | |||
| 42 | // 地形の高さ(0より下は水面で平らにする) | ||
| 43 | float terrainHeight(vec2 xz) { | ||
| 44 | return max(3.0 * fbm(xz * 0.3) - 1.1, 0.0); | ||
| 45 | } | ||
| 46 | |||
| 47 | void main() { | ||
| 48 | // gl_TessCoord:パッチの中の位置(u, v)、0〜1 | ||
| 49 | vec2 uv = gl_TessCoord.xy; | ||
| 50 | vec3 bottom = mix(tcPosition[0], tcPosition[1], uv.x); | ||
| 51 | vec3 top = mix(tcPosition[3], tcPosition[2], uv.x); | ||
| 52 | vec3 p = mix(bottom, top, uv.y); | ||
| 53 | |||
| 54 | // 高さで押し上げる(ディスプレイスメント) | ||
| 55 | p.y = terrainHeight(p.xz); | ||
| 56 | |||
| 57 | // 法線:高さの差分から(y = h(x, z) の勾配) | ||
| 58 | const float e = 0.02; | ||
| 59 | float hx = terrainHeight(p.xz + vec2(e, 0.0)) - terrainHeight(p.xz - vec2(e, 0.0)); | ||
| 60 | float hz = terrainHeight(p.xz + vec2(0.0, e)) - terrainHeight(p.xz - vec2(0.0, e)); | ||
| 61 | vNormal = normalize(vec3(-hx, 2.0 * e, -hz)); | ||
| 62 | |||
| 63 | vWorldPos = p; | ||
| 64 | gl_Position = uProj * uView * vec4(p, 1.0); | ||
| 65 | } | ||
ファイル: /dev/null | b/shader/vertex_passthrough.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,16 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | layout(location = 0) in vec3 aPosition; | ||
| 4 | layout(location = 1) in vec3 aNormal; | ||
| 5 | layout(location = 2) in vec2 aUv; | ||
| 6 | |||
| 7 | // 何も変換せず、そのままジオメトリシェーダーへ渡す | ||
| 8 | out vec3 vsPosition; | ||
| 9 | out vec3 vsNormal; | ||
| 10 | out vec2 vsUv; | ||
| 11 | |||
| 12 | void main() { | ||
| 13 | vsPosition = aPosition; | ||
| 14 | vsNormal = aNormal; | ||
| 15 | vsUv = aUv; | ||
| 16 | } | ||
ファイル: /dev/null | b/shader/vertex_terrain.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -0,0 +1,9 @@ | |||
| 1 | #version 450 core | ||
| 2 | |||
| 3 | layout(location = 0) in vec3 aPosition; | ||
| 4 | |||
| 5 | out vec3 vsPosition; // パッチの角(ワールド空間) | ||
| 6 | |||
| 7 | void main() { | ||
| 8 | vsPosition = aPosition; | ||
| 9 | } | ||
コメント(0)
まだコメントはありません。
ログインするとコメント出来ます。