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𝐧を、同じビュー・投影行列で投影します。
こうすると、物体の大きさに関係なく、全ての線が本当に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章):

𝐧face=normalize((𝐩1−𝐩0)×(𝐩2−𝐩0))

反時計回りの巻き順(第13章)なら、此れはメッシュの外を向きます。
+Zから見て反時計回りの三角形(0,0,0),(1,0,0),(0,1,0)で確かめると、(1,0,0)×(0,1,0)=(0,0,1)で、見ている人の方を向きます。

#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);

デモ2:左はジオメトリシェーダーで欠片に割れた球、右は法線を線で描いた球

欠片は三角形ではなく小さな四角形に見えます。
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個の四角形で、四角形毎に(0,0),(1,0),(1,1),(0,1)の順に4つのインデックスがあります(関数はDIFFにあります。第13章のcreateGridと同じ仕組みですが、xz平面上で、セル毎に6つではなく4つのインデックスです)。
パッチは僅か64個、頂点は81個で、残りはテッセレーターが作ります。

仕事は3つに分かれます:

  1. テッセレーション制御シェーダー(TCS)は、各パッチの出力制御点毎に1回実行されます。制御点を其のまま渡すか変更し、gl_TessLevelOuterとgl_TessLevelInnerでパッチをどれ位細かく分割するかを決めます。
  2. 固定のテッセレーターは、抽象的なパッチの領域(此処では単位正方形)を沢山の小さな三角形に切り、新しい頂点毎に其の領域の中の位置gl_TessCoordを作ります。
  3. テッセレーション評価シェーダー(TES)は新しい頂点毎に1回実行され、gl_TessCoordとパッチの制御点から本当の位置を求め、gl_Positionを書きます。

テッセレーションレベル

四角形のパッチには、辺毎に1つ、合わせて4つの外側のレベルと、2つの内側のレベルがあります:

四角形の領域:outer[0]〜outer[3]は辺u = 0、v = 0、u = 1、v = 1に対応し、inner[0]とinner[1]は内側をuとvの方向に分割する

  • gl_TessLevelOuter[0]〜[3]:各辺を何個に切るか。辺0はu=0、辺1はv=0、辺2はu=1、辺3はv=1です。
  • gl_TessLevelInner[0]と[1]:内側をu方向とv方向にどれ位細かく切るか。

外側のレベルが分かれているのには、とても良い理由があります。
隣り合う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単位の辺は96/8=12個に、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つの角の双線型補間です。
先ず下と上の辺でu方向に、次に其の2点の間をv方向に補間します(mixを2段階、第11章のバイリニアフィルタリングと同じです)。
例えば角が(0,0,0)、(1.25,0,0)、(1.25,0,1.25)、(0,0,1.25)のパッチで(u,v)=(0.25,0.5)なら、下 =(0.3125,0,0)、上 =(0.3125,0,1.25)で、mixの結果は(0.3125,0,0.625)です。

次に第8章のfBmノイズ(4オクターブ)から高さを求めます。
max(..., 0.0)は0より下を全て平らな湖にします。
法線は第14章と同じく中心差分から求めます。
表面はy=h(x,z)、つまりF(x,y,z)=y−h(x,z)=0で、其の勾配は∇F=(−hx,1,−hz)です。
hx≈Δx/2e(Δxはコードの差分hx)なので、ベクトル全体に2eを掛けると(−Δx,2e,−Δz)になり、方向は同じで割り算が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番目より小さくない場合の結果は未定義です。

デモ3:テッセレーションで細かくし、変位させた地形

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)はx×y×z個のワークグループの格子を開始します。

各呼び出しには組み込みのIDがあります:

gl_GlobalInvocationID=gl_WorkGroupID⋅gl_WorkGroupSize+gl_LocalInvocationID

(成分毎、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なので、横に⌈200/16⌉=13個、縦に⌈150/16⌉=10個のグループが必要です。
Cの整数の計算で切り上げるには(n + 15) / 16です。
(200+15)/16=13、(150+15)/16=10。
此れで13⋅16×10⋅16=208×160個の呼び出しが起動し、セルより少し多いですが、先頭のreturnで余った物は何もしません:

200 × 150の画像を16 × 16個の呼び出しのワークグループ13 × 10個で覆う。赤い帯は画像の外

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();

デモ4:数百世代後のライフゲーム、紫の残像付き

ランダムなノイズから、数秒でお馴染みの形が現れます。
ブリンカー、ブロック、そして画面を斜めに這って行く(端で反対側に回り込む)グライダーです。
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 課題

  1. ★ ライフゲームの格子を320 × 200にし、ワークグループを8 × 8にしました。glDispatchComputeに何を渡し、何もしない呼び出しは幾つありますか(40 × 25グループで、何もしない呼び出しは0になるはずです)?
  2. ★ 本文のedgeLevelで、中点がカメラから4単位の辺は幾つに分割されますか?又、どの距離で辺は1つの区間まで落ちますか(24個、96単位以上)?
  3. ★★ 法線のジオメトリシェーダーを拡張し、各三角形の面の法線も、三角形の中心から3番目の色で描いて下さい。max_verticesを8に上げるのを忘れずに。
  4. ★★ ジオメトリシェーダーでワイヤーフレームを重ねて下さい。各三角形の3つの頂点に重心座標(1,0,0)、(0,1,0)、(0,0,1)を渡し、フラグメントシェーダーで其の一番小さい値が0.02未満のピクセルを暗くします。陰影の付いた立方体と球の上に、全ての三角形の辺が暗い線として見えるはずです。
  5. ★★ 地形でfractional_odd_spacingをequal_spacingに変え、カメラが動く間ワイヤーフレームを見て下さい。三角形が滑らかではなく、急に飛ぶ様に変わるはずです。
  6. ★★ ライフゲームの規則を「HighLife」(死んでいるセルは隣が6つでも生まれる)に変えて下さい。見た目は似ていますが、時々パターンが自分自身を複製するはずです。
  7. ★★★ コンピュートシェーダーのパーティクルシステムを書いて下さい。10 000個の位置と速度のバッファ(layout(std430, binding = 0) buffer Particles { vec4 data[]; };)、重力を加えて床で跳ね返らせるコンピュートシェーダー、glMemoryBarrier(GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT)、そして同じバッファをGL_POINTSとして描きます。CPUに一度も触れない点の噴水が見えるはずです。

DIFFをダウンロード

ファイル: 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)

まだコメントはありません。