13. 頂点シェーダーと3D

此れまでの頂点シェーダーは殆ど退屈な物でした。
既に正規化デバイス座標にある4つの角を受け取り、其のまま渡すだけで、面白い作業は全てフラグメントシェーダーで行っていました。
此の章では、遂に頂点シェーダーに本当の仕事を与えます。
本物の3Dの物体(立方体、潰した球、波打つ旗)を描き、行列で動かし、カメラで投影し、更にジオメトリ自体をアニメーションさせます。

此の章で線型代数シリーズの成果が出ます。
行列・ベクトル積、同次座標、逆行列と転置、lookAtと透視投影行列が全て再登場し、今度は全ての頂点毎にGPU上で動作します。

始める前に:プロジェクトフォルダーのコピーを作って下さい。
第14章は第5章のシェーダープレイグラウンドに戻り、第15章は此の章のコードの続きなので、両方のバージョンを残しておくと便利です。

13.1 頂点シェーダーが本当にやっている事

GPUは描画する頂点毎に1回頂点シェーダーを実行します。
毎回、其の頂点の属性(位置、法線、UV等、layout(location = N)付きのin変数)を受け取り、必ず1つの物を書き込まなければなりません。
其れはgl_Position、つまりクリップ空間での頂点の位置です。
其の他にout変数に書いた物はラスタライザーに渡され、三角形の中で補間されてから、フラグメントシェーダーのin変数になります(第3章で見ました)。

メッシュに保存されている位置は、物体自身の座標系での位置です。
其れをクリップ空間に持って行くには、線型代数シリーズの第11章と同じ空間の連鎖を通ります:

頂点が通る空間と、各ステップを誰が行うか

行列の3つのステップはあたし達の担当です。
Cで行列を作り、頂点シェーダーで掛けます。
最後の2つのステップは頂点シェーダーの後にGPUが行います。
wで割り(第12章の透視除算)、glViewportで設定したビューポートでNDCをピクセルに変換します。

オブジェクト空間の位置𝐩にある頂点を数式1行で書くと:

𝐩clip=𝐏𝐕𝐌[pxpypz1]

𝐌はモデル行列(オブジェクト → ワールド)、𝐕はビュー行列(ワールド → ビュー)、𝐏は投影行列(ビュー → クリップ)です。
4番目の成分の1は、平行移動を可能にする同次座標です(線型代数の第9章)。

此の章の最後には此の様になります:

完成したシーン:チェッカー模様の立方体、潰れた青い球、グラデーションの空の前で波打つ旗

此の章の新しいファイル:

ファイル 内容
include/matrix.h C言語の小さな行列ライブラリ(ヘッダーのみ)
shader/vertex_mesh.glsl 3Dの頂点シェーダー(モデル、ビュー、投影、法線行列)
shader/frag_phong.glsl UV模様付きのピクセル毎のBlinn-Phongライティング
shader/vertex_gouraud.glsl、shader/frag_gouraud.glsl 同じライティングを頂点毎に
shader/vertex_flag.glsl 頂点アニメーション:波打つ旗
shader/vertex_fullscreen.glsl、shader/frag_sky.glsl 頂点バッファなしの背景

Xcodeユーザーへ:shader/の新しいファイルを、既存のシェーダーファイルと同じ様にプロジェクトと「Copy Files」ビルドフェーズに追加して下さい。
追加しないとアプリがファイルを見つけられません。

13.2 C言語の小さな行列ライブラリ

シェーダーが行列を掛けますが、先ず誰かが行列を作らなければなりません。
其の為に小さなヘッダーinclude/matrix.hを追加します。
線型代数シリーズの約束事に完全に従います。
列ベクトル、右手座標系、−Zを向くカメラ、そして列優先(column-major)の格納です。

typedef struct {
  float m[16];
} Matrix4;

typedef struct {
  float m[9];
} Matrix3;

// 行row・列colの要素(0始まり)
static inline float *mat4At(Matrix4 *a, int row, int col) {
  return &a->m[col * 4 + row];
}

列優先とは、16個のfloatが列毎に順番に並ぶという意味です。
m[0..3]が1列目、m[4..7]が2列目、と続きます。
此れはGLSLのmat4と同じレイアウトなので(第2章で見た通り、添字はm[列][行]の順)、並べ替えずにOpenGLに渡せます。

2つの行列の積は線型代数の第7章の通常の物です。
i行j列の要素は、𝐀のi行目と𝐁のj列目の内積です。

// 行列の積 r = a * b(bを先に適用する)
static inline Matrix4 mat4Multiply(Matrix4 a, Matrix4 b) {
  Matrix4 r;
  for (int col = 0; col < 4; col++) {
    for (int row = 0; row < 4; row++) {
      float sum = 0.0f;
      for (int k = 0; k < 4; k++) sum += a.m[k * 4 + row] * b.m[col * 4 + k];
      r.m[col * 4 + row] = sum;
    }
  }
  return r;
}

平行移動は移動量を4列目に入れ(線型代数の第9章)、回転は第8章の物です:

// 平行移動
static inline Matrix4 mat4Translate(float x, float y, float z) {
  Matrix4 r = mat4Identity();
  r.m[12] = x; // 4列目
  r.m[13] = y;
  r.m[14] = z;
  return r;
}

// Y軸周りの回転
static inline Matrix4 mat4RotateY(float angle) {
  float c = cosf(angle), s = sinf(angle);
  Matrix4 r = mat4Identity();
  *mat4At(&r, 0, 0) = c;  *mat4At(&r, 0, 2) = s;
  *mat4At(&r, 2, 0) = -s; *mat4At(&r, 2, 2) = c;
  return r;
}

ヘッダーにはmat4Identity、mat4Scale、mat4RotateX(同じ考え方)と、2つのカメラ行列もあります。
mat4LookAtは目の位置、注視点、上方向ベクトルからビュー行列を作ります。
線型代数の第11章のlookAtと1行ずつ同じです(ベクトルの計算は小さな補助関数vec3Sub、vec3Dot、vec3Cross、vec3Normalizeが行います):

// ビュー行列:カメラがeyeからtargetを見る
static inline Matrix4 mat4LookAt(Vector3 eye, Vector3 target, Vector3 up) {
  Vector3 f = vec3Normalize(vec3Sub(target, eye)); // 前
  Vector3 r = vec3Normalize(vec3Cross(f, up));     // 右
  Vector3 u = vec3Cross(r, f);                     // 上

  Matrix4 m = mat4Identity();
  *mat4At(&m, 0, 0) = r.x;  *mat4At(&m, 0, 1) = r.y;  *mat4At(&m, 0, 2) = r.z;
  *mat4At(&m, 1, 0) = u.x;  *mat4At(&m, 1, 1) = u.y;  *mat4At(&m, 1, 2) = u.z;
  *mat4At(&m, 2, 0) = -f.x; *mat4At(&m, 2, 1) = -f.y; *mat4At(&m, 2, 2) = -f.z;
  *mat4At(&m, 0, 3) = -vec3Dot(r, eye);
  *mat4At(&m, 1, 3) = -vec3Dot(u, eye);
  *mat4At(&m, 2, 3) = vec3Dot(f, eye);
  return m;
}

mat4Perspectiveは線型代数の第12章のOpenGL用透視投影行列です。
t=1/tan(fovY/2)、縦横比a、ニア面n、ファー面fとすると:

𝐏=[t/a0000t0000−f+nf−n−2fnf−n00−10]

// 透視投影(OpenGL:NDCのzは-1〜1)、fovYは縦の視野角(ラジアン)
static inline Matrix4 mat4Perspective(float fovY, float aspect, float n, float f) {
  float t = 1.0f / tanf(fovY * 0.5f);
  Matrix4 m = { { 0 } };
  *mat4At(&m, 0, 0) = t / aspect;
  *mat4At(&m, 1, 1) = t;
  *mat4At(&m, 2, 2) = -(f + n) / (f - n);
  *mat4At(&m, 2, 3) = -(2.0f * f * n) / (f - n);
  *mat4At(&m, 3, 2) = -1.0f;
  return m;
}

行列をシェーダーに送る

行列はユニフォームとしてglUniformMatrix4fv(3×3ならglUniformMatrix3fv)でシェーダーに送ります:

// 行列のユニフォームを設定(列優先のままなのでtransposeはGL_FALSE)
void setMat4(GLuint program, const char *name, Matrix4 m) {
  glUniformMatrix4fv(glGetUniformLocation(program, name), 1, GL_FALSE, m.m);
}

void setMat3(GLuint program, const char *name, Matrix3 m) {
  glUniformMatrix3fv(glGetUniformLocation(program, name), 1, GL_FALSE, m.m);
}

3番目の引数は「OpenGLが転置しましょうか?」という意味です。
OpenGLは列優先のデータを期待していて、あたし達のデータは既に列優先なので、答えはGL_FALSEです。
行優先で格納している場合(多くのC++の数学ライブラリやDirectX風のコードがそうです)、代わりにGL_TRUEを渡すか、自分で転置します。
此処を間違えてもクラッシュはしません。
平行移動が一番下の行に入ってwに掛かってしまうので、物体が変な方向に飛んで行くだけです。

計算例:1つの頂点を画面まで

数字が魔法に見えない様に、立方体の角を1つ手で追ってみましょう。
立方体の角はオブジェクト空間で𝐩=(0.5,0.5,0.5)、モデル行列は立方体を(−1.7,0.1,0)だけ左上に動かすだけ、そして(数字を簡単にする為に)カメラは(0,0,5)から原点を見ているとします。
其の場合、ビュー行列は(0,0,−5)の平行移動だけです。
投影はfovY = 50°、a=800/600=4/3、n=0.1、f=100です:

t=1tan25∘=2.1445,ta=1.6084,−f+nf−n=−1.0020,−2fnf−n=−0.2002

ワールド空間:(0.5−1.7,0.5+0.1,0.5)=(−1.2,0.6,0.5)。
ビュー空間:(−1.2,0.6,0.5−5)=(−1.2,0.6,−4.5)。
つまりカメラの4.5単位前にあります(前はzが負の方向)。
クリップ空間を1行ずつ:

xc=1.6084⋅(−1.2)=−1.9301yc=2.1445⋅0.6=1.2867zc=−1.0020⋅(−4.5)−0.2002=4.3088wc=−1⋅(−4.5)=4.5

此れが頂点シェーダーがgl_Positionに書く値です。
次にGPUがwcで割ります。
NDC =(−1.9301,1.2867,4.3088)/4.5=(−0.4289,0.2859,0.9575)。
最後にビューポートが[−1,1]をピクセルに変換します。
x=(−0.4289+1)/2⋅800=228.4、y=(0.2859+1)/2⋅600=385.8(ウィンドウの下から)。
zの0.9575は深度バッファに入り、直ぐ後で必要になります。
実際のプログラムではカメラは(0,1,5)にあり、少し下の(0,0.1,0)を見ているので、ビュー行列は少し綺麗ではなくなりますが、手順は同じです。

13.3 立方体のメッシュ

立方体には角が8つありますが、24個の頂点を保存します。
6つの面毎に4つずつです。
理由は法線です。
頂点は位置だけではなく属性の束全体であり、3つの面が出会う角は、平らで角の立った面にする為に3つの違う法線(面毎に1つ)が必要です。
其の為、各面に専用の4頂点を持たせます。

全ての頂点は8個のfloatです。
位置(3)、法線(3)、UV(2)。
小さな補助関数が其の様な配列をアップロードし、3つの属性をlocation 0、1、2に設定します:

// メッシュ(頂点配列オブジェクトとインデックスの数)
typedef struct {
  GLuint vao, vbo, ebo;
  GLsizei indexCount;
} Mesh;

// 頂点の形式:位置(3)、法線(3)、UV(2)=8個のfloat
#define VERTEX_FLOATS 8

Mesh createMesh(const float *vertices, int vertexCount, const GLuint *indices, int indexCount) {
  Mesh mesh;
  mesh.indexCount = indexCount;
  glGenVertexArrays(1, &mesh.vao);
  glGenBuffers(1, &mesh.vbo);
  glGenBuffers(1, &mesh.ebo);

  glBindVertexArray(mesh.vao);
  glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
  glBufferData(GL_ARRAY_BUFFER, vertexCount * VERTEX_FLOATS * sizeof(float), vertices, GL_STATIC_DRAW);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * sizeof(GLuint), indices, GL_STATIC_DRAW);

  // 頂点属性:位置(location 0)、法線(location 1)、UV(location 2)
  GLsizei stride = VERTEX_FLOATS * sizeof(float);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void *)0);
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void *)(3 * sizeof(float)));
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void *)(6 * sizeof(float)));
  glEnableVertexAttribArray(2);

  glBindVertexArray(0);
  return mesh;
}

void drawMesh(const Mesh *mesh) {
  glBindVertexArray(mesh->vao);
  glDrawElements(GL_TRIANGLES, mesh->indexCount, GL_UNSIGNED_INT, 0);
}

(destroyMeshが最後に3つのオブジェクトを削除します。)
24 × 8個の数字を打ち込む代わりに、各面を法線𝐧と面上の2つの方向𝐮(右)と𝐯(上)で表します。
すると4つの角は12(𝐧±𝐮±𝐯)になり、一辺1で中心が原点の立方体が出来ます。

// 立方体(一辺1、中心が原点):各面に4頂点(面ごとに法線が違う為)
Mesh createCube(void) {
  // 各面:法線n、右方向u、上方向v(u × v = n なので外から見て反時計回り)
  const float faces[6][9] = {
    {  1, 0, 0,   0, 0, -1,   0, 1, 0 }, // +X
    { -1, 0, 0,   0, 0, 1,    0, 1, 0 }, // -X
    {  0, 1, 0,   1, 0, 0,    0, 0, -1 }, // +Y
    {  0, -1, 0,  1, 0, 0,    0, 0, 1 }, // -Y
    {  0, 0, 1,   1, 0, 0,    0, 1, 0 }, // +Z
    {  0, 0, -1,  -1, 0, 0,   0, 1, 0 }, // -Z
  };
  const float corners[4][2] = { { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } };

  float vertices[24 * VERTEX_FLOATS];
  GLuint indices[36];
  for (int f = 0; f < 6; f++) {
    const float *n = faces[f], *u = faces[f] + 3, *v = faces[f] + 6;
    for (int c = 0; c < 4; c++) {
      float s = corners[c][0], t = corners[c][1];
      float *out = &vertices[(f * 4 + c) * VERTEX_FLOATS];
      for (int k = 0; k < 3; k++) out[k] = 0.5f * (n[k] + s * u[k] + t * v[k]); // 位置
      for (int k = 0; k < 3; k++) out[3 + k] = n[k];                            // 法線
      out[6] = 0.5f * (s + 1.0f);                                               // UV
      out[7] = 0.5f * (t + 1.0f);
    }
    GLuint base = f * 4;
    GLuint quad[6] = { base, base + 1, base + 2, base + 2, base + 3, base };
    memcpy(&indices[f * 6], quad, sizeof(quad));
  }
  return createMesh(vertices, 24, indices, 36);
}

𝐮と𝐯の順番は適当ではありません。
𝐮×𝐯=𝐧になる様に選びました(線型代数の第5章の外積)。
例えば+Xの面では(0,0,−1)×(0,1,0)=(0⋅0−(−1)⋅1,(−1)⋅0−0⋅0,0⋅1−0⋅0)=(1,0,0)です。
此の規則があると、角(−,−),(+,−),(+,+),(−,+)は外から見て反時計回りに並びます。
OpenGLは此の巻き順で三角形の表と裏を区別します。
後で裏面カリングに使います。

13.4 最初の3D頂点シェーダー

此れがshader/vertex_mesh.glslの最初のバージョンです。
3つの属性を全て読みますが、今は法線だけを渡します:

#version 450 core

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aUv;

out vec3 vNormal;

uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProj;

void main() {
  vNormal = aNormal;
  gl_Position = uProj * uView * uModel * vec4(aPosition, 1.0);
}

vec4(aPosition, 1.0)は3Dの位置をw=1の同次座標の点にします。
方向なら平行移動の影響を受けない様にw=0にします。
此の考え方は直ぐに法線で使います。

GLSLでは、mat4とvec4の間の*は本物の行列・ベクトル積で(vec4 * vec4の様な成分毎の積ではありません)、2つのmat4の間では行列の積です。
uProj * uView * uModel * vはCの*の連鎖と同じく左から右へ評価されます。
其の為、GPUは先ずuProj * uView(4×4掛ける4×4、64回の掛け算)を計算し、次にuModelを掛け(更に64回)、最後にベクトルを掛けます(16回)。
uProj * (uView * (uModel * v))と書けば、結果は全く同じですが(行列の積は結合法則を満たします)、行列・ベクトル積3回、つまり48回の掛け算で済みます。
立方体1つなら全く関係なく、ドライバーのコンパイラーが並べ替えてくれる事もありますが、良い習慣です。
又、此れが実際のエンジンが普通uProj * uView * uModelをCPUで1回だけ計算し、1つのuMVP行列を送る理由です。
下の最終版では、ライティングに必要なのでワールド空間の位置を別に保持します。

フラグメントシェーダーには第5章の技を使います。
データを色として表示します。
法線の成分は−1〜1なので、𝐧⋅0.5+0.5で0〜1に変換します:

#version 450 core

in vec3 vNormal;
out vec4 out_color;

void main() {
  // 法線(-1〜1)を色(0〜1)にする
  out_color = vec4(normalize(vNormal) * 0.5 + 0.5, 1.0);
}

C側では毎フレームカメラを作り、立方体を描きます:

    // カメラ:ビュー行列と投影行列
    Vector3 eye = { { 0.0f, 1.0f, 5.0f } };
    Vector3 target = { { 0.0f, 0.1f, 0.0f } };
    Vector3 up = { { 0.0f, 1.0f, 0.0f } };
    Matrix4 view = mat4LookAt(eye, target, up);
    float aspect = gHeight > 0 ? (float)gWidth / (float)gHeight : 1.0f;
    Matrix4 proj = mat4Perspective(50.0f * PI / 180.0f, aspect, 0.1f, 100.0f);

    glUseProgram(program);
    setMat4(program, "uView", view);
    setMat4(program, "uProj", proj);
    Matrix4 model = mat4Multiply(mat4RotateY(time * 0.7f + 0.5f), mat4RotateX(0.5f));
    model = mat4Multiply(mat4Translate(0.0f, 0.1f, 0.0f), mat4Multiply(model, mat4Scale(1.6f, 1.6f, 1.6f)));
    setMat4(program, "uModel", model);
    drawMesh(&cube);

(PIはmain.cの先頭の#defineです。
M_PIは標準Cの一部ではなく、MSVCはデフォルトで定義しない為です。)
縦横比は第5章のフレームバッファの大きさを使うので、ウィンドウの大きさを変えてもRetinaディスプレイでも立方体は立方体のままです。

深度バッファ

実行すると左の画像になります:

左:深度テストなしでは、裏の面が表の面の上に描かれる。右:深度テストあり。

何かが大きく間違っています。
三角形は単にインデックスバッファの順番で描かれ、後の三角形は前の三角形の上に塗られます。
例え其の後ろにあってもです。
面は+X、−X、+Y、−Y、+Z、−Zの順に保存されているので、奥にある−Yと−Zの面が最後に描かれ、表の面を覆ってしまいます。

解決策は深度バッファ(Zバッファ)です。
ピクセル毎に1つの深度値を持つ2枚目の画像です。
計算例のzの0.9575を覚えていますか?
GPUはNDCのzを0〜1の深度に変換し、深度テストが有効なら、其のピクセルに既に保存されている物より近い場合だけフラグメントを残します。

  // 深度テストと裏面カリング
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_CULL_FACE);
  glCullFace(GL_BACK);
  glFrontFace(GL_CCW);

そして毎フレーム、深度バッファも色と一緒にクリアしなければなりません。
さもないと前のフレームの深度が全てを遮ります:

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

GL_CULL_FACEはおまけです。
画面上で巻き順が時計回りに見える三角形はカメラから背を向けているので、GPUはラスタライズの前に捨てます。
立方体の様な閉じたメッシュでは其れらは絶対に見えないので、作業が半分になります。
glFrontFace(GL_CCW)は反時計回りが表という意味で、だから面の巻き順が大事だったのです。
此れで立方体は正しく見えます(上の右の画像)。
+Yは緑(0.5,1,0.5)、+Xならピンクがかった赤、左の面の−Xは青緑(0,0.5,0.5)です。

13.5 法線と法線行列

ライティングには、光の方向やカメラの位置と同じく、ワールド空間の法線が必要です。
すぐ思い付くのはモデル行列を掛ける事です。
mat3(uModel) * aNormal(mat3(...)コンストラクターはmat4の左上3×3を取り出し、方向が無視すべき平行移動を落とします)。
回転と均一な拡大縮小なら此れで動きますが、軸毎に違う拡大縮小をすると直ぐに壊れます:

xを2倍にすると表面が傾き、S nはもう垂直ではないが、逆行列の転置は垂直のまま

式

法線の正しい行列は、モデル行列の左上3×3の部分の逆行列の転置です:

𝐍=(𝐌3×3−1)⊤,𝐧world=𝐍𝐧

何故此れで正しいか

法線は1つの性質で定義されます。
表面の全ての接ベクトル𝐭に垂直である事、𝐧⋅𝐭=𝐧⊤𝐭=0です。
接ベクトルは表面上の近い点同士の差なので、位置と同じ様に変換されます。
𝐭′=𝐌𝐭。
新しい法線𝐆𝐧が新しい接ベクトルにまだ垂直になる様な行列𝐆が欲しいです:

(𝐆𝐧)⊤(𝐌𝐭)=𝐧⊤𝐆⊤𝐌𝐭

𝐆⊤𝐌=𝐈なら、此れは全ての接ベクトルについて𝐧⊤𝐭=0になり、正に欲しい物です。
従って𝐆⊤=𝐌−1、つまり𝐆=(𝐌−1)⊤です(逆行列は線型代数の第10章、転置は第7章)。
2つの特別な場合が、素朴なやり方でも大抵上手く行く様に見える理由を説明します:

  • 純粋な回転𝐑は𝐑−1=𝐑⊤なので(𝐑−1)⊤=𝐑。法線行列は回転其の物です。
  • s倍の均一な拡大縮小では1s𝐈になり、長さが変わるだけで、どうせ正規化します。

計算例

あたし達の球は𝐒=diag(0.9,0.6,0.9)で潰されています。
単位球上の点(0,0.6,0.8)を取ります。
其の法線は同じベクトル𝐧=(0,0.6,0.8)で、接ベクトルの1つは𝐭=(0,0.8,−0.6)です(確認:0.6⋅0.8−0.8⋅0.6=0)。
潰した後、接ベクトルは𝐒𝐭=(0,0.48,−0.54)になります。

素朴な法線は𝐒𝐧=(0,0.36,0.72)で:

(𝐒𝐭)⋅(𝐒𝐧)=0.48⋅0.36+(−0.54)⋅0.72=0.1728−0.3888=−0.216≠0

従って、もう表面に垂直ではありません。
対角行列の逆行列の転置は、対角の逆数を取るだけです。
diag(1/0.9,1/0.6,1/0.9)なので、正しい法線は(0,0.6/0.6,0.8/0.9)=(0,1,0.8889)で:

(𝐒𝐭)⋅𝐍𝐧=0.48⋅1+(−0.54)⋅0.8889=0.48−0.48=0

画面上の違いは此の通りです(ライティングのシェーダーは次の節です):

上:モデル行列で変換した法線。下:法線行列で変換した法線。

上の物は、丸い球の陰影を平らな形に描いた様に見えます。
ハイライトは高過ぎる位置にあり、下側もまだ照らされています。
下の物は本当の形、平たい小石の様に見えます。
光は広い上面に当たり、下側は暗いです。

Cで計算する

GLSLにはinverse()とtranspose()があるので(mat2、mat3、mat4用)、シェーダーでtranspose(inverse(mat3(uModel)))と書いても動きます。
然し、物体全体で同じなのに頂点毎に計算し直す事になるので、Cで物体毎に1回だけ計算します。
便利な近道があります。
逆行列は余因子行列(余因子の行列の転置)を行列式で割った物なので、逆行列の転置は余因子の行列を行列式で割った物になり、何も転置する必要がありません。

// 法線行列:モデル行列の左上3×3の逆行列の転置
// 逆行列の転置 = 余因子行列 / 行列式 なので、余因子を直接計算する
static inline Matrix3 mat3NormalMatrix(Matrix4 a) {
  // 左上3×3の要素(行, 列)
  float m00 = a.m[0], m10 = a.m[1], m20 = a.m[2];
  float m01 = a.m[4], m11 = a.m[5], m21 = a.m[6];
  float m02 = a.m[8], m12 = a.m[9], m22 = a.m[10];

  // 余因子
  float c00 = m11 * m22 - m12 * m21, c01 = m12 * m20 - m10 * m22, c02 = m10 * m21 - m11 * m20;
  float c10 = m02 * m21 - m01 * m22, c11 = m00 * m22 - m02 * m20, c12 = m01 * m20 - m00 * m21;
  float c20 = m01 * m12 - m02 * m11, c21 = m02 * m10 - m00 * m12, c22 = m00 * m11 - m01 * m10;

  float det = m00 * c00 + m01 * c01 + m02 * c02;
  float inv = det != 0.0f ? 1.0f / det : 0.0f;

  // 結果の(行i, 列j) = c_ij / det、列優先で格納
  Matrix3 r;
  r.m[0] = c00 * inv; r.m[1] = c10 * inv; r.m[2] = c20 * inv;
  r.m[3] = c01 * inv; r.m[4] = c11 * inv; r.m[5] = c21 * inv;
  r.m[6] = c02 * inv; r.m[7] = c12 * inv; r.m[8] = c22 * inv;
  return r;
}

行列式は1行目に沿った余因子展開です(線型代数の第10章)。
此の関数を、適当な回転・拡大縮小・平行移動の行列で数値的な逆行列の転置と比べました。
最大の差は10−6未満でした。

最終版の頂点シェーダーは、ワールド空間の位置、法線、UVを渡します:

#version 450 core

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aUv;

out vec3 vWorldPos; // ワールド空間の位置
out vec3 vNormal;   // ワールド空間の法線
out vec2 vUv;

uniform mat4 uModel;        // オブジェクト空間 → ワールド空間
uniform mat4 uView;         // ワールド空間 → ビュー空間
uniform mat4 uProj;         // ビュー空間 → クリップ空間
uniform mat3 uNormalMatrix; // 法線用(モデル行列の逆行列の転置)

void main() {
  vec4 worldPos = uModel * vec4(aPosition, 1.0);
  vWorldPos = worldPos.xyz;
  vNormal = uNormalMatrix * aNormal;
  vUv = aUv;
  gl_Position = uProj * (uView * worldPos);
}

最後の行の括弧に注目して下さい。
worldPosは既にあるので、行列・ベクトル積2回で済みます。

13.6 ワールド空間でのライティング

フラグメントシェーダーは第9章のBlinn-Phongモデルに、第10章の半球アンビエントと最後のガンマ補正を加えた物で、今度はワールド空間で計算します。
法線、光の方向、カメラの位置が全て同じ空間にある事、其れがライティングで本当に大事な唯一の規則です。

#version 450 core

in vec3 vWorldPos;
in vec3 vNormal;
in vec2 vUv;
out vec4 out_color;

uniform vec3 uColor;     // 物体の色(リニア)
uniform vec3 uLightDir;  // 表面から光の方へ向かう方向(ワールド空間)
uniform vec3 uCameraPos; // カメラの位置(ワールド空間)
uniform int uPattern;    // 0:単色、1:チェッカー、2:日の丸

// UVから物体の色を決める
vec3 albedo() {
  if (uPattern == 1) {
    vec2 cell = floor(vUv * 4.0);
    float checker = mod(cell.x + cell.y, 2.0);
    return uColor * mix(0.55, 1.0, checker);
  }
  if (uPattern == 2) {
    vec2 p = (vUv - 0.5) * vec2(1.4 / 0.9, 1.0); // 旗の縦横比
    float disc = 1.0 - smoothstep(0.29, 0.31, length(p));
    return mix(uColor, vec3(0.75, 0.02, 0.05), disc);
  }
  return uColor;
}

void main() {
  vec3 n = normalize(vNormal);
  if (!gl_FrontFacing) n = -n; // 裏面(旗の裏側)では法線を反転
  vec3 l = normalize(uLightDir);
  vec3 v = normalize(uCameraPos - vWorldPos);
  vec3 h = normalize(l + v); // ハーフベクトル(Blinn-Phong)

  float diffuse = max(dot(n, l), 0.0);
  float specular = diffuse > 0.0 ? pow(max(dot(n, h), 0.0), 64.0) : 0.0;

  // 空と地面の色の半球ライト(アンビエント)
  vec3 ambient = mix(vec3(0.05, 0.04, 0.03), vec3(0.10, 0.13, 0.20), n.y * 0.5 + 0.5);

  vec3 color = albedo() * (ambient + diffuse) + vec3(0.6) * specular;
  out_color = vec4(pow(color, vec3(1.0 / 2.2)), 1.0); // ガンマ補正
}

新しい点が幾つかあります:

  • 法線行列の結果が正しくても、もう一度normalize(vNormal)します。単位ベクトル同士を補間すると三角形の真ん中では長さが1より短くなり、法線行列自体も長さを保ちません。
  • uLightDirは表面から光の方へ向かうので、照らされた面でdot(n, l)が正になり、マイナス記号は要りません(もう一つの約束事は第4章で説明しています)。
  • gl_FrontFacingはフラグメントシェーダーの組み込みのboolです。此のフラグメントの三角形がカメラを向いていれば(glFrontFaceの設定で、画面上で反時計回りなら)trueです。旗は両側から見える1枚のシートなので、裏側では法線を反転します。
  • UVの模様には第7章のfloorとmodを使います。floor(vUv * 4.0)は4×4の格子のセル番号で、mod(cell.x + cell.y, 2.0)は0と1を交互に繰り返します。旗の赤い円は第6章の円のSDFを旗の縦横比で引き伸ばした物です。
  • uPatternはintのユニフォームで、glUniform1iで設定します。

C側では2つの補助関数がユニフォームを設定します。
drawLitは物体毎に法線行列も計算します:

// 照明付きの物体を描く(モデル行列から法線行列も計算する)
void drawLit(GLuint program, const Mesh *mesh, Matrix4 model, Vector3 color, int pattern) {
  setMat4(program, "uModel", model);
  setMat3(program, "uNormalMatrix", mat3NormalMatrix(model));
  glUniform3f(glGetUniformLocation(program, "uColor"), color.r, color.g, color.b);
  glUniform1i(glGetUniformLocation(program, "uPattern"), pattern);
  drawMesh(mesh);
}

// 全てのプログラムで共通のユニフォーム
void setCommonUniforms(GLuint program, Matrix4 view, Matrix4 proj, Vector3 eye, float time) {
  glUseProgram(program);
  setMat4(program, "uView", view);
  setMat4(program, "uProj", proj);
  glUniform3f(glGetUniformLocation(program, "uCameraPos"), eye.x, eye.y, eye.z);
  glUniform3f(glGetUniformLocation(program, "uLightDir"), -0.5f, 0.8f, 0.6f); // 光の方へ向かう方向
  glUniform1f(glGetUniformLocation(program, "uTime"), time);
}

そして物体で、其々独自のモデル行列を持ちます。
線型代数シリーズと同じく、積は右から左に読みます。
𝐌=𝐓𝐑𝐒は先ず拡大縮小、次に回転、最後に移動です。

    // 立方体:回転してから左へ移動(M = T * R)
    Matrix4 cubeModel = mat4Multiply(mat4Translate(-1.7f, 0.1f, 0.0f),
                                     mat4Multiply(mat4RotateY(time * 0.7f), mat4RotateX(0.6f)));
    Vector3 cubeColor = { { 0.9f, 0.45f, 0.1f } };
    drawLit(lit, &cube, cubeModel, cubeColor, 1);

    // 球:わざと潰してから回転(M = T * R * S)、法線行列が必要になる
    Matrix4 sphereModel = mat4Multiply(mat4Translate(0.0f, 0.1f, 0.0f),
                                       mat4Multiply(mat4RotateY(time * 0.5f),
                                                    mat4Scale(0.9f, 0.6f, 0.9f)));
    Vector3 sphereColor = { { 0.2f, 0.45f, 0.9f } };
    drawLit(lit, &sphere, sphereModel, sphereColor, 1);

    // 旗竿:細長く伸ばした立方体
    Matrix4 poleModel = mat4Multiply(mat4Translate(1.2f, 0.0f, 0.0f), mat4Scale(0.05f, 2.2f, 0.05f));
    Vector3 poleColor = { { 0.8f, 0.8f, 0.75f } };
    drawLit(lit, &cube, poleModel, poleColor, 0);

旗竿は不均一な拡大縮小の良い使い道です。
同じ立方体のメッシュを0.05 × 2.2 × 0.05に伸ばしただけです。
球はcreateSphere(16, 12)で作るUV球です。
スタックi、スライスjの角度はφ=πi/stacksとθ=2πj/slicesで、点は(sinφsinθ,cosφ,sinφcosθ)です。
単位球では法線は位置其の物なので、法線はタダで手に入ります。
コードはDIFFにあります。
2重ループで頂点を作り、小さな四角形を其々反時計回りの三角形2つで繋ぎます。

13.7 頂点毎とピクセル毎のライティング

ライティングは何処で計算すべきでしょうか?
今はピクセル毎に計算しました。
頂点シェーダーが法線を渡し、ラスタライザーが補間し、フラグメントシェーダーが全てのピクセルを照らします。
此れをフォンシェーディングと呼びます(フォンの反射モデルと混同しないで下さい。同じ名前が両方に使われます)。

古いやり方のグーローシェーディングは、頂点シェーダーでライティングを計算し、結果の色だけを補間します:

#version 450 core

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aUv;

out vec3 vColor; // 頂点で計算したライティングの結果

uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProj;
uniform mat3 uNormalMatrix;
uniform vec3 uColor;
uniform vec3 uLightDir;
uniform vec3 uCameraPos;

void main() {
  vec4 worldPos = uModel * vec4(aPosition, 1.0);
  vec3 n = normalize(uNormalMatrix * aNormal);
  vec3 l = normalize(uLightDir);
  vec3 v = normalize(uCameraPos - worldPos.xyz);
  vec3 h = normalize(l + v);

  float diffuse = max(dot(n, l), 0.0);
  float specular = diffuse > 0.0 ? pow(max(dot(n, h), 0.0), 64.0) : 0.0;
  vec3 ambient = mix(vec3(0.05, 0.04, 0.03), vec3(0.10, 0.13, 0.20), n.y * 0.5 + 0.5);

  vColor = uColor * (ambient + diffuse) + vec3(0.6) * specular;
  gl_Position = uProj * (uView * worldPos);
}

#version 450 core

in vec3 vColor; // ラスタライザーが三角形の中で補間した色
out vec4 out_color;

void main() {
  out_color = vec4(pow(vColor, vec3(1.0 / 2.2)), 1.0);
}

(補間がリニア空間で行われる様に、ガンマ補正はフラグメントシェーダーに残します。)
プログラムでGキーを押すと2つを切り替えられます:

同じローポリの球。左:グーロー、頂点毎のライティング。右:フォン、ピクセル毎のライティング。

拡散反射の部分は表面上でゆっくり変化するので、殆ど同じに見えます。
違いが分かるのは鏡面ハイライトです。
小さく鋭いピークなので、頂点が少ないとグーロー版は見逃すか、三角形の辺に沿って星型の塊に広げてしまい、物体が回転すると泳ぐ様に動きます。
ピクセル毎のライティングは、メッシュがどれ程粗くても丸いハイライトになります。

では何故グーローがあったのでしょうか?
モデルの頂点は覆うピクセルよりずっと少ないので、遥かに安く、何十年も其れが重要でした(シェーダー以前の固定機能OpenGLのライティングはグーローでした)。
今はピクセル毎のライティングが標準で、頂点シェーダーは主に其の為のデータを準備する為に使われます。

13.8 頂点アニメーション:波打つ旗

頂点シェーダーは行列を掛ける以上の事が出来ます。
毎フレーム全ての頂点を好きな場所に、殆どコストなしで動かせます。
ゲームは此の方法で水、風に揺れる草、布等をアニメーションさせます。

1つだけ問題があります。
頂点シェーダーが動かせるのは存在する頂点だけです。
三角形2つの旗は傾く事は出来ても、波打つ事は出来ません。
其の為、細かく分割した平面が必要です。
createGrid(32, 20, 1.4f, 0.9f)で作ります。
32 × 20個の小さな四角形で、大きさは1.4 × 0.9単位、XY平面上にあり、左端がx=0(竿がある所)です。
頂点シェーダーが扱う物が見える様に、シーン全体をワイヤーフレームで表示すると(glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)):

ワイヤーフレーム表示:立方体は12個、球は数百個、旗は千個以上の三角形

波

旗に沿って進むサイン波で各頂点をz方向に押し、振幅を竿の0から自由端(幅W)のAまで大きくします:

h(x,y)=a(x)sinϕ,a(x)=AxW,ϕ=kx+cy−ωt

kはx方向に1単位あたり波が何ラジアン進むか、cは波面が完全に垂直にならない様に少し傾け、ωはラジアン毎秒の速さです。

法線

頂点を動かすと表面が変わるので、古い法線(0,0,1)は間違いになります。
幸い、表面は正確に分かっています。
𝐩(x,y)=(x,y,h(x,y))です。
2つの接ベクトルは偏微分∂𝐩/∂x=(1,0,hx)と∂𝐩/∂y=(0,1,hy)で(hxは∂h/∂xの意味)、其の外積が法線です:

(1,0,hx)×(0,1,hy)=(0⋅hy−hx⋅1,hx⋅0−1⋅hy,1⋅1−0⋅0)=(−hx,−hy,1)

積の微分法則を使うと(振幅もxに依存するので):

hx=AWsinϕ+a(x)kcosϕ,hy=a(x)ccosϕ

#version 450 core

layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aUv;

out vec3 vWorldPos;
out vec3 vNormal;
out vec2 vUv;

uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProj;
uniform mat3 uNormalMatrix;
uniform float uTime;

const float AMPLITUDE = 0.15; // 波の高さ(旗の端で)
const float WAVE_K = 5.0;     // 波数(x方向に1あたり何ラジアン)
const float WAVE_Y = 1.5;     // 斜めの波にする為のy方向の波数
const float SPEED = 4.0;      // 角速度(ラジアン/秒)
const float WIDTH = 1.4;      // 旗の幅(C側の格子と同じ)

void main() {
  vec3 p = aPosition;

  // 竿(x = 0)では動かず、端に行く程大きく揺れる
  float a = AMPLITUDE * p.x / WIDTH;
  float phase = WAVE_K * p.x + WAVE_Y * p.y - SPEED * uTime;
  p.z = a * sin(phase);

  // 法線:曲面 z = h(x, y) の偏微分から (-dh/dx, -dh/dy, 1)
  float dhdx = AMPLITUDE / WIDTH * sin(phase) + a * WAVE_K * cos(phase);
  float dhdy = a * WAVE_Y * cos(phase);
  vec3 normal = normalize(vec3(-dhdx, -dhdy, 1.0));

  vec4 worldPos = uModel * vec4(p, 1.0);
  vWorldPos = worldPos.xyz;
  vNormal = uNormalMatrix * normal;
  vUv = aUv;
  gl_Position = uProj * (uView * worldPos);
}

此処ではaNormalは全く使いません(使わない入力はコンパイラーが落とします)。
フラグメントシェーダーは同じfrag_phong.glslで、uPattern = 2です。
旗は両側から見える1枚のシートなので、描く間はカリングを無効にし(だからgl_FrontFacingが必要でした)、波が見える様に旗を竿の周りで少し回します:

    // 旗:頂点シェーダーで波打たせる(両面を描くのでカリングなし)
    GLuint flagProgram = gPrograms[PROGRAM_FLAG].id;
    setCommonUniforms(flagProgram, view, proj, eye, time);
    glDisable(GL_CULL_FACE);
    Vector3 flagColor = { { 0.95f, 0.95f, 0.92f } };
    Matrix4 flagModel = mat4Multiply(mat4Translate(1.2f, 0.2f, 0.0f), mat4RotateY(0.6f));
    drawLit(flagProgram, &flag, flagModel, flagColor, 2);
    glEnable(GL_CULL_FACE);

此のアニメーションはCPUに全く負担を掛けない事に注目して下さい。
バッファの中のメッシュは変わらず、送るのはfloat1つ、uTimeだけです。

13.9 gl_VertexID:バッファなしのフルスクリーン三角形

此の章の最後の技は背景です。
第5章から、プレイグラウンドはバッファの4頂点の四角形で描いてきました。
殆ど全てのエンジンで見かける、もっと安い方法があります。
頂点バッファを全く使わず、画面全体を覆う大きな三角形1つです。

gl_VertexIDは頂点シェーダーの組み込みのint入力で、処理中の頂点の番号です(glDrawArraysなら0、1、2、…)。
此れから角を自分で計算出来ます:

#version 450 core

out vec2 vUv;

// 頂点バッファなしで画面全体を覆う大きな三角形
// gl_VertexID = 0, 1, 2 → (-1, -1), (3, -1), (-1, 3)
void main() {
  vec2 uv = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
  vUv = uv;
  gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0);
}

<<はビットを左にずらし、&は両方の数で立っているビットだけを残します(Cと同じ整数の演算子です):

gl_VertexID (id << 1) & 2 id & 2 uv gl_Position.xy
0 0 & 2 = 0 0 (0, 0) (−1, −1)
1 2 & 2 = 2 0 (2, 0) (3, −1)
2 4 & 2 = 0 2 (0, 2) (−1, 3)

三角形 (−1, −1)、(3, −1)、(−1, 3) はNDCの正方形全体を覆う

画面の外の部分はタダでクリッピングされ、画面の中ではvUvが四角形のUVと同じく0〜1になります。
何故わざわざ?
四角形は対角線を共有する2つの三角形で、GPUはピクセルを2×2の小さなブロックでシェーディングするので、対角線沿いのピクセルが2回シェーディングされます。
三角形1つなら其れを避けられ、バッファも要りません。
空1つなら殆ど関係ありませんが、ポストプロセスのパスは1フレームに何度もフルスクリーンのシェーダーを実行するので、積み重なります。

フラグメントシェーダーは第5章のgl_FragCoordを使った簡単な縦のグラデーションです:

#version 450 core

in vec2 vUv;
out vec4 out_color;

uniform vec2 uResolution;

void main() {
  float t = gl_FragCoord.y / uResolution.y; // 0(下)〜1(上)
  vec3 horizon = vec3(0.32, 0.30, 0.36);
  vec3 zenith = vec3(0.07, 0.10, 0.20);
  vec3 color = mix(horizon, zenith, smoothstep(0.0, 1.0, t));
  out_color = vec4(color, 1.0);
}

コアプロファイルでは、属性が1つもなくても描画には何らかのVAOがバインドされている必要があるので、空のVAOを1回作り、glDrawArraysで描きます。
空は深度テストを無効にして最初に描くので、何も隠さず、深度も書きません:

  // 頂点バッファなしで描く為の空のVAO(コアプロファイルではVAOが必須)
  GLuint emptyVAO;
  glGenVertexArrays(1, &emptyVAO);

    // 背景:頂点バッファなしのフルスクリーン三角形(深度テストなし)
    glDisable(GL_DEPTH_TEST);
    GLuint sky = gPrograms[PROGRAM_SKY].id;
    glUseProgram(sky);
    glUniform2f(glGetUniformLocation(sky, "uResolution"), (float)gWidth, (float)gHeight);
    glBindVertexArray(emptyVAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    glEnable(GL_DEPTH_TEST);

(エンジンでは、深度テストで物体に覆われたピクセルを全て飛ばせる様に、空をGL_LEQUALでファー面に最後に描く事が多いです。)

13.10 複数のプログラムとホットリロード

シェーダープログラムは4つになりました。
空、フォン、グーロー、旗です。
第5章のRキーのホットリロードを全てで使える様に、Cのコードはプログラムを表に入れ、全部作り直します:

// シェーダープログラムの一覧(Rキーで全部再読み込み)
typedef struct {
  const char *vertexPath;
  const char *fragmentPath;
  GLuint id;
} Program;

enum { PROGRAM_SKY, PROGRAM_PHONG, PROGRAM_GOURAUD, PROGRAM_FLAG, PROGRAM_COUNT };

static Program gPrograms[PROGRAM_COUNT] = {
  { "shader/vertex_fullscreen.glsl", "shader/frag_sky.glsl", 0 },
  { "shader/vertex_mesh.glsl", "shader/frag_phong.glsl", 0 },
  { "shader/vertex_gouraud.glsl", "shader/frag_gouraud.glsl", 0 },
  { "shader/vertex_flag.glsl", "shader/frag_phong.glsl", 0 },
};

// 全てのプログラムを作り直す(失敗した物は前のままにする)、失敗の数を返す
int loadPrograms(void) {
  int failed = 0;
  for (int i = 0; i < PROGRAM_COUNT; i++) {
    GLuint program = buildProgram(gPrograms[i].vertexPath, gPrograms[i].fragmentPath);
    if (program) {
      if (gPrograms[i].id) glDeleteProgram(gPrograms[i].id);
      gPrograms[i].id = program;
    } else {
      failed++;
    }
  }
  return failed;
}

buildProgramはmacOSの#versionの書き換えも含めて第5章から変わっておらず、此の章の全てがOpenGL 4.1で動くので、Macユーザーも章全体を試せます。
main.cの残り(メッシュの作成、Gキー、描画の順番)は最後のDIFFにあります。

13.11 此の章の新しいGLSL

機能 内容
gl_Position 頂点シェーダーの出力:クリップ空間の位置。GPUがwで割る
mat4 * vec4、mat4 * mat4 本物の行列の積(列ベクトル、右から左に適用)
uniform mat4、uniform mat3 Cからの行列。glUniformMatrix4fv/glUniformMatrix3fvで設定
mat3(m4) mat4の左上3×3(平行移動を落とす)
inverse(m)、transpose(m) 逆行列と転置(頂点毎だと重いので、CPUで計算する方が良い)
gl_FrontFacing フラグメントシェーダーのbool:三角形がカメラを向いているか
gl_VertexID 頂点シェーダーのint:現在の頂点の番号
<<、& 整数のビットシフトとビット毎のAND
uniform int 整数のユニフォーム。glUniform1iで設定

13.12 実際のゲームやエンジンでの使い方

  • モデル・ビュー・投影は全ての3Dレンダラーの心臓部です。エンジンは普通、物体毎にCPUで行列を合成してMVPを送り、ライティング用にモデル行列と法線行列も送ります。多くの場合ユニフォームバッファに詰め込みます。
  • スケルタルアニメーション(スキニング):各頂点が最大4本のボーンの番号と4つの重みを持ち、頂点シェーダーがボーンの行列を混ぜます。𝐩′=∑iwi𝐁i𝐩。ゲームで見たアニメーションするキャラクターは全て此の仕組みです。
  • 風と水:草、葉、旗は頂点シェーダーのサイン波で揺れ、普通は頂点カラーに塗った値で重み付けします(あたし達の竿側の端の様に根元は動きません)。海面は幾つもの動く波を重ねます(ゲルストナー波がよく使われます)。
  • インスタンシング:glDrawElementsInstancedで何千本もの木や岩を1回の呼び出しで描き、頂点シェーダーはgl_InstanceIDで其々のコピーの変換を選びます。
  • ビルボードとパーティクルは頂点シェーダーでカメラの方を向かせます。
  • フルスクリーン三角形は全てのポストプロセスのパスで使われます。ブルーム、トーンマッピング、カラーグレーディング、アンチエイリアス。
  • グーローシェーディングは全てのサイクルが大事な所や、意図的なレトロな見た目(PlayStation 1時代を真似たゲーム)で今も使われます。

13.13 まとめ

  • 頂点シェーダーは頂点毎に1回実行され、クリップ空間のgl_Positionを必ず書きます。其の後GPUがwで割り、ビューポートを適用します。
  • gl_Position = P * V * M * vec4(pos, 1.0):モデル・ビュー・投影行列はCで作り(matrix.h、GLSLと同じ列優先)、glUniformMatrix4fv(..., GL_FALSE, ...)で送ります。
  • 積はP * (V * (M * v))と括弧を付けるか、もっと良いのはCPUで行列を合成する事です。
  • 深度バッファ(GL_DEPTH_TEST、毎フレームクリア)が前後関係を解決し、裏面カリングが反時計回りの巻き順を使って背を向けた三角形を捨てます。
  • 法線はモデル行列の逆行列の転置で変換します。拡大縮小が不均一になった途端に重要になります。CPUで物体毎に1回計算します。
  • グーローシェーディングは頂点毎にライティングして色を補間し(安いがハイライトが悪い)、フォンシェーディングは法線を補間してピクセル毎にライティングします。
  • 頂点シェーダーはジオメトリをアニメーション出来ますが(旗)、存在する頂点だけです。法線は変位の微分から計算し直す必要があります。
  • gl_VertexIDを使えば、空のVAOをバインドするだけで頂点バッファなしにフルスクリーン三角形を描けます。

13.14 課題

  1. ★ モデル行列が(2,1,1)倍に拡大縮小します。接ベクトル𝐭=(1,−1,0)を持つ表面の法線𝐧=(1,1,0)を、モデル行列と法線行列で其々変換し、どちらが変換後の接ベクトルに垂直か確かめて下さい(片方は内積が3、もう片方は0になるはずです)。
  2. ★ 13.2の計算例で、立方体の角を(−0.5,0.5,0.5)に変え、NDCのxとピクセルのxを手で計算して下さい(左端から約85ピクセルの所になるはずです)。
  3. ★★ ウィンドウを作る前にglfwWindowHint(GLFW_SAMPLES, 4);を、GLADを読み込んだ後にglEnable(GL_MULTISAMPLE);を追加して下さい。立方体と竿のギザギザの縁が滑らかになるはずです(マルチサンプルアンチエイリアス)。
  4. ★★ マウスでカメラを回せる様にして下さい。mouseX / windowWから角度を計算し、目を(5sinα,1,5cosα)に置きます。マウスを左右に動かすとシーンの周りを回り、ライティングはカメラではなく物体に付いたままになるはずです。
  5. ★★ 旗に、違うWAVE_KとSPEEDの小さな2つ目の波を加え、其の微分をdhdxとdhdyに足して下さい。一時的にn * 0.5 + 0.5を色として出力して法線を確認します。継ぎ目なく滑らかに変化するはずです。
  6. ★★ 球をcreateSphere(64, 48)に変えてGキーを押して下さい。グーロー版とフォン版が殆ど同じに見えるはずです。昔はハイポリのモデルがグーローの弱点を隠していた理由です。
  7. ★★★ 10 × 10個の立方体を、glDrawElementsInstancedの1回の呼び出しで描いて下さい。頂点シェーダーでgl_InstanceIDからオフセットを計算し(vec3(id % 10, 0, id / 10)を拡大して中心に寄せる)、ワールド空間の位置に足します。1回の描画呼び出しで100個の立方体が見えるはずです。

DIFFをダウンロード

ファイル: /dev/null | b/include/matrix.h

前新
@@ -0,0 +1,149 @@
    1 #pragma once
    2  
    3 // 小さな行列ライブラリ(線型代数シリーズと同じ約束事)
    4 // ・列優先(column-major):m[列 * 4 + 行]
    5 // ・右手座標系、カメラは-Z方向を向く
    6 // ・ベクトルは列ベクトル:v' = M * v
    7  
    8 #include <math.h>
    9 #include <vector.h>
    10  
    11 typedef struct {
    12 float m[16];
    13 } Matrix4;
    14  
    15 typedef struct {
    16 float m[9];
    17 } Matrix3;
    18  
    19 // ベクトルの補助関数
    20 static inline Vector3 vec3Sub(Vector3 a, Vector3 b) {
    21 return (Vector3){ { a.x - b.x, a.y - b.y, a.z - b.z } };
    22 }
    23  
    24 static inline float vec3Dot(Vector3 a, Vector3 b) {
    25 return a.x * b.x + a.y * b.y + a.z * b.z;
    26 }
    27  
    28 static inline Vector3 vec3Cross(Vector3 a, Vector3 b) {
    29 return (Vector3){ { a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x } };
    30 }
    31  
    32 static inline Vector3 vec3Normalize(Vector3 v) {
    33 float len = sqrtf(vec3Dot(v, v));
    34 if (len == 0.0f) return v;
    35 return (Vector3){ { v.x / len, v.y / len, v.z / len } };
    36 }
    37  
    38 // 行row・列colの要素(0始まり)
    39 static inline float *mat4At(Matrix4 *a, int row, int col) {
    40 return &a->m[col * 4 + row];
    41 }
    42  
    43 // 単位行列
    44 static inline Matrix4 mat4Identity(void) {
    45 Matrix4 r = { { 0 } };
    46 r.m[0] = r.m[5] = r.m[10] = r.m[15] = 1.0f;
    47 return r;
    48 }
    49  
    50 // 行列の積 r = a * b(bを先に適用する)
    51 static inline Matrix4 mat4Multiply(Matrix4 a, Matrix4 b) {
    52 Matrix4 r;
    53 for (int col = 0; col < 4; col++) {
    54 for (int row = 0; row < 4; row++) {
    55 float sum = 0.0f;
    56 for (int k = 0; k < 4; k++) sum += a.m[k * 4 + row] * b.m[col * 4 + k];
    57 r.m[col * 4 + row] = sum;
    58 }
    59 }
    60 return r;
    61 }
    62  
    63 // 平行移動
    64 static inline Matrix4 mat4Translate(float x, float y, float z) {
    65 Matrix4 r = mat4Identity();
    66 r.m[12] = x; // 4列目
    67 r.m[13] = y;
    68 r.m[14] = z;
    69 return r;
    70 }
    71  
    72 // 拡大縮小
    73 static inline Matrix4 mat4Scale(float x, float y, float z) {
    74 Matrix4 r = mat4Identity();
    75 r.m[0] = x;
    76 r.m[5] = y;
    77 r.m[10] = z;
    78 return r;
    79 }
    80  
    81 // X軸周りの回転(ラジアン、右手の法則で反時計回り)
    82 static inline Matrix4 mat4RotateX(float angle) {
    83 float c = cosf(angle), s = sinf(angle);
    84 Matrix4 r = mat4Identity();
    85 *mat4At(&r, 1, 1) = c; *mat4At(&r, 1, 2) = -s;
    86 *mat4At(&r, 2, 1) = s; *mat4At(&r, 2, 2) = c;
    87 return r;
    88 }
    89  
    90 // Y軸周りの回転
    91 static inline Matrix4 mat4RotateY(float angle) {
    92 float c = cosf(angle), s = sinf(angle);
    93 Matrix4 r = mat4Identity();
    94 *mat4At(&r, 0, 0) = c; *mat4At(&r, 0, 2) = s;
    95 *mat4At(&r, 2, 0) = -s; *mat4At(&r, 2, 2) = c;
    96 return r;
    97 }
    98  
    99 // ビュー行列:カメラがeyeからtargetを見る
    100 static inline Matrix4 mat4LookAt(Vector3 eye, Vector3 target, Vector3 up) {
    101 Vector3 f = vec3Normalize(vec3Sub(target, eye)); // 前
    102 Vector3 r = vec3Normalize(vec3Cross(f, up)); // 右
    103 Vector3 u = vec3Cross(r, f); // 上
    104  
    105 Matrix4 m = mat4Identity();
    106 *mat4At(&m, 0, 0) = r.x; *mat4At(&m, 0, 1) = r.y; *mat4At(&m, 0, 2) = r.z;
    107 *mat4At(&m, 1, 0) = u.x; *mat4At(&m, 1, 1) = u.y; *mat4At(&m, 1, 2) = u.z;
    108 *mat4At(&m, 2, 0) = -f.x; *mat4At(&m, 2, 1) = -f.y; *mat4At(&m, 2, 2) = -f.z;
    109 *mat4At(&m, 0, 3) = -vec3Dot(r, eye);
    110 *mat4At(&m, 1, 3) = -vec3Dot(u, eye);
    111 *mat4At(&m, 2, 3) = vec3Dot(f, eye);
    112 return m;
    113 }
    114  
    115 // 透視投影(OpenGL:NDCのzは-1〜1)、fovYは縦の視野角(ラジアン)
    116 static inline Matrix4 mat4Perspective(float fovY, float aspect, float n, float f) {
    117 float t = 1.0f / tanf(fovY * 0.5f);
    118 Matrix4 m = { { 0 } };
    119 *mat4At(&m, 0, 0) = t / aspect;
    120 *mat4At(&m, 1, 1) = t;
    121 *mat4At(&m, 2, 2) = -(f + n) / (f - n);
    122 *mat4At(&m, 2, 3) = -(2.0f * f * n) / (f - n);
    123 *mat4At(&m, 3, 2) = -1.0f;
    124 return m;
    125 }
    126  
    127 // 法線行列:モデル行列の左上3×3の逆行列の転置
    128 // 逆行列の転置 = 余因子行列 / 行列式 なので、余因子を直接計算する
    129 static inline Matrix3 mat3NormalMatrix(Matrix4 a) {
    130 // 左上3×3の要素(行, 列)
    131 float m00 = a.m[0], m10 = a.m[1], m20 = a.m[2];
    132 float m01 = a.m[4], m11 = a.m[5], m21 = a.m[6];
    133 float m02 = a.m[8], m12 = a.m[9], m22 = a.m[10];
    134  
    135 // 余因子
    136 float c00 = m11 * m22 - m12 * m21, c01 = m12 * m20 - m10 * m22, c02 = m10 * m21 - m11 * m20;
    137 float c10 = m02 * m21 - m01 * m22, c11 = m00 * m22 - m02 * m20, c12 = m01 * m20 - m00 * m21;
    138 float c20 = m01 * m12 - m02 * m11, c21 = m02 * m10 - m00 * m12, c22 = m00 * m11 - m01 * m10;
    139  
    140 float det = m00 * c00 + m01 * c01 + m02 * c02;
    141 float inv = det != 0.0f ? 1.0f / det : 0.0f;
    142  
    143 // 結果の(行i, 列j) = c_ij / det、列優先で格納
    144 Matrix3 r;
    145 r.m[0] = c00 * inv; r.m[1] = c10 * inv; r.m[2] = c20 * inv;
    146 r.m[3] = c01 * inv; r.m[4] = c11 * inv; r.m[5] = c21 * inv;
    147 r.m[6] = c02 * inv; r.m[7] = c12 * inv; r.m[8] = c22 * inv;
    148 return r;
    149 }

ファイル: a/main.c | b/main.c

前新
@@ -11,6 +11,7 @@
11 #include <stdlib.h> 11 #include <stdlib.h>
12 #include <string.h> 12 #include <string.h>
13 #include <vector.h> 13 #include <vector.h>
    14 #include <matrix.h>
14 #include <math.h> 15 #include <math.h>
15   16  
16 // ファイルパスを構築(プラットフォームに応じて) 17 // ファイルパスを構築(プラットフォームに応じて)
@@ -176,6 +177,220 @@ GLuint buildProgram(const char *vertexPath, const char *fragmentPath) {
176 return program; 177 return program;
177 } 178 }
178   179  
    180 #define PI 3.14159265358979f
    181  
    182 // メッシュ(頂点配列オブジェクトとインデックスの数)
    183 typedef struct {
    184 GLuint vao, vbo, ebo;
    185 GLsizei indexCount;
    186 } Mesh;
    187  
    188 // 頂点の形式:位置(3)、法線(3)、UV(2)=8個のfloat
    189 #define VERTEX_FLOATS 8
    190  
    191 // 頂点とインデックスの配列からメッシュを作る
    192 Mesh createMesh(const float *vertices, int vertexCount, const GLuint *indices, int indexCount) {
    193 Mesh mesh;
    194 mesh.indexCount = indexCount;
    195 glGenVertexArrays(1, &mesh.vao);
    196 glGenBuffers(1, &mesh.vbo);
    197 glGenBuffers(1, &mesh.ebo);
    198  
    199 glBindVertexArray(mesh.vao);
    200 glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo);
    201 glBufferData(GL_ARRAY_BUFFER, vertexCount * VERTEX_FLOATS * sizeof(float), vertices, GL_STATIC_DRAW);
    202 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh.ebo);
    203 glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * sizeof(GLuint), indices, GL_STATIC_DRAW);
    204  
    205 // 頂点属性:位置(location 0)、法線(location 1)、UV(location 2)
    206 GLsizei stride = VERTEX_FLOATS * sizeof(float);
    207 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void *)0);
    208 glEnableVertexAttribArray(0);
    209 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void *)(3 * sizeof(float)));
    210 glEnableVertexAttribArray(1);
    211 glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void *)(6 * sizeof(float)));
    212 glEnableVertexAttribArray(2);
    213  
    214 glBindVertexArray(0);
    215 return mesh;
    216 }
    217  
    218 void drawMesh(const Mesh *mesh) {
    219 glBindVertexArray(mesh->vao);
    220 glDrawElements(GL_TRIANGLES, mesh->indexCount, GL_UNSIGNED_INT, 0);
    221 }
    222  
    223 void destroyMesh(Mesh *mesh) {
    224 glDeleteVertexArrays(1, &mesh->vao);
    225 glDeleteBuffers(1, &mesh->vbo);
    226 glDeleteBuffers(1, &mesh->ebo);
    227 }
    228  
    229 // 立方体(一辺1、中心が原点):各面に4頂点(面ごとに法線が違う為)
    230 Mesh createCube(void) {
    231 // 各面:法線n、右方向u、上方向v(u × v = n なので外から見て反時計回り)
    232 const float faces[6][9] = {
    233 { 1, 0, 0, 0, 0, -1, 0, 1, 0 }, // +X
    234 { -1, 0, 0, 0, 0, 1, 0, 1, 0 }, // -X
    235 { 0, 1, 0, 1, 0, 0, 0, 0, -1 }, // +Y
    236 { 0, -1, 0, 1, 0, 0, 0, 0, 1 }, // -Y
    237 { 0, 0, 1, 1, 0, 0, 0, 1, 0 }, // +Z
    238 { 0, 0, -1, -1, 0, 0, 0, 1, 0 }, // -Z
    239 };
    240 const float corners[4][2] = { { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } };
    241  
    242 float vertices[24 * VERTEX_FLOATS];
    243 GLuint indices[36];
    244 for (int f = 0; f < 6; f++) {
    245 const float *n = faces[f], *u = faces[f] + 3, *v = faces[f] + 6;
    246 for (int c = 0; c < 4; c++) {
    247 float s = corners[c][0], t = corners[c][1];
    248 float *out = &vertices[(f * 4 + c) * VERTEX_FLOATS];
    249 for (int k = 0; k < 3; k++) out[k] = 0.5f * (n[k] + s * u[k] + t * v[k]); // 位置
    250 for (int k = 0; k < 3; k++) out[3 + k] = n[k]; // 法線
    251 out[6] = 0.5f * (s + 1.0f); // UV
    252 out[7] = 0.5f * (t + 1.0f);
    253 }
    254 GLuint base = f * 4;
    255 GLuint quad[6] = { base, base + 1, base + 2, base + 2, base + 3, base };
    256 memcpy(&indices[f * 6], quad, sizeof(quad));
    257 }
    258 return createMesh(vertices, 24, indices, 36);
    259 }
    260  
    261 // UV球(半径1):slices=経度の分割数、stacks=緯度の分割数
    262 Mesh createSphere(int slices, int stacks) {
    263 int vertexCount = (slices + 1) * (stacks + 1);
    264 int indexCount = slices * stacks * 6;
    265 float *vertices = malloc(vertexCount * VERTEX_FLOATS * sizeof(float));
    266 GLuint *indices = malloc(indexCount * sizeof(GLuint));
    267  
    268 float *out = vertices;
    269 for (int i = 0; i <= stacks; i++) {
    270 float phi = PI * (float)i / (float)stacks; // 0(北極)〜π(南極)
    271 for (int j = 0; j <= slices; j++) {
    272 float theta = 2.0f * PI * (float)j / (float)slices;
    273 float x = sinf(phi) * sinf(theta);
    274 float y = cosf(phi);
    275 float z = sinf(phi) * cosf(theta);
    276 // 単位球なので、位置と法線は同じ
    277 out[0] = x; out[1] = y; out[2] = z;
    278 out[3] = x; out[4] = y; out[5] = z;
    279 out[6] = (float)j / (float)slices;
    280 out[7] = 1.0f - (float)i / (float)stacks;
    281 out += VERTEX_FLOATS;
    282 }
    283 }
    284  
    285 GLuint *idx = indices;
    286 for (int i = 0; i < stacks; i++) {
    287 for (int j = 0; j < slices; j++) {
    288 GLuint a = i * (slices + 1) + j; // 左上
    289 GLuint b = a + slices + 1; // 左下
    290 *idx++ = a; *idx++ = b; *idx++ = a + 1;
    291 *idx++ = a + 1; *idx++ = b; *idx++ = b + 1;
    292 }
    293 }
    294  
    295 Mesh mesh = createMesh(vertices, vertexCount, indices, indexCount);
    296 free(vertices);
    297 free(indices);
    298 return mesh;
    299 }
    300  
    301 // XY平面上の格子(幅w、高さh、左下が原点):nx × ny個の四角形
    302 Mesh createGrid(int nx, int ny, float w, float h) {
    303 int vertexCount = (nx + 1) * (ny + 1);
    304 int indexCount = nx * ny * 6;
    305 float *vertices = malloc(vertexCount * VERTEX_FLOATS * sizeof(float));
    306 GLuint *indices = malloc(indexCount * sizeof(GLuint));
    307  
    308 float *out = vertices;
    309 for (int j = 0; j <= ny; j++) {
    310 for (int i = 0; i <= nx; i++) {
    311 float u = (float)i / (float)nx, v = (float)j / (float)ny;
    312 out[0] = u * w; out[1] = v * h; out[2] = 0.0f;
    313 out[3] = 0.0f; out[4] = 0.0f; out[5] = 1.0f;
    314 out[6] = u; out[7] = v;
    315 out += VERTEX_FLOATS;
    316 }
    317 }
    318  
    319 GLuint *idx = indices;
    320 for (int j = 0; j < ny; j++) {
    321 for (int i = 0; i < nx; i++) {
    322 GLuint a = j * (nx + 1) + i; // 左下
    323 GLuint b = a + nx + 1; // 左上
    324 *idx++ = a; *idx++ = a + 1; *idx++ = b + 1;
    325 *idx++ = b + 1; *idx++ = b; *idx++ = a;
    326 }
    327 }
    328  
    329 Mesh mesh = createMesh(vertices, vertexCount, indices, indexCount);
    330 free(vertices);
    331 free(indices);
    332 return mesh;
    333 }
    334  
    335 // 行列のユニフォームを設定(列優先のままなのでtransposeはGL_FALSE)
    336 void setMat4(GLuint program, const char *name, Matrix4 m) {
    337 glUniformMatrix4fv(glGetUniformLocation(program, name), 1, GL_FALSE, m.m);
    338 }
    339  
    340 void setMat3(GLuint program, const char *name, Matrix3 m) {
    341 glUniformMatrix3fv(glGetUniformLocation(program, name), 1, GL_FALSE, m.m);
    342 }
    343  
    344 // シェーダープログラムの一覧(Rキーで全部再読み込み)
    345 typedef struct {
    346 const char *vertexPath;
    347 const char *fragmentPath;
    348 GLuint id;
    349 } Program;
    350  
    351 enum { PROGRAM_SKY, PROGRAM_PHONG, PROGRAM_GOURAUD, PROGRAM_FLAG, PROGRAM_COUNT };
    352  
    353 static Program gPrograms[PROGRAM_COUNT] = {
    354 { "shader/vertex_fullscreen.glsl", "shader/frag_sky.glsl", 0 },
    355 { "shader/vertex_mesh.glsl", "shader/frag_phong.glsl", 0 },
    356 { "shader/vertex_gouraud.glsl", "shader/frag_gouraud.glsl", 0 },
    357 { "shader/vertex_flag.glsl", "shader/frag_phong.glsl", 0 },
    358 };
    359  
    360 // 全てのプログラムを作り直す(失敗した物は前のままにする)、失敗の数を返す
    361 int loadPrograms(void) {
    362 int failed = 0;
    363 for (int i = 0; i < PROGRAM_COUNT; i++) {
    364 GLuint program = buildProgram(gPrograms[i].vertexPath, gPrograms[i].fragmentPath);
    365 if (program) {
    366 if (gPrograms[i].id) glDeleteProgram(gPrograms[i].id);
    367 gPrograms[i].id = program;
    368 } else {
    369 failed++;
    370 }
    371 }
    372 return failed;
    373 }
    374  
    375 // 照明付きの物体を描く(モデル行列から法線行列も計算する)
    376 void drawLit(GLuint program, const Mesh *mesh, Matrix4 model, Vector3 color, int pattern) {
    377 setMat4(program, "uModel", model);
    378 setMat3(program, "uNormalMatrix", mat3NormalMatrix(model));
    379 glUniform3f(glGetUniformLocation(program, "uColor"), color.r, color.g, color.b);
    380 glUniform1i(glGetUniformLocation(program, "uPattern"), pattern);
    381 drawMesh(mesh);
    382 }
    383  
    384 // 全てのプログラムで共通のユニフォーム
    385 void setCommonUniforms(GLuint program, Matrix4 view, Matrix4 proj, Vector3 eye, float time) {
    386 glUseProgram(program);
    387 setMat4(program, "uView", view);
    388 setMat4(program, "uProj", proj);
    389 glUniform3f(glGetUniformLocation(program, "uCameraPos"), eye.x, eye.y, eye.z);
    390 glUniform3f(glGetUniformLocation(program, "uLightDir"), -0.5f, 0.8f, 0.6f); // 光の方へ向かう方向
    391 glUniform1f(glGetUniformLocation(program, "uTime"), time);
    392 }
    393  
179 int main(int argc, char **argv) { 394 int main(int argc, char **argv) {
180 (void)argc; 395 (void)argc;
181 (void)argv; 396 (void)argv;
@@ -202,7 +417,7 @@ int main(int argc, char **argv) {
202 #endif 417 #endif
203   418  
204 // ウィンドウを作成 419 // ウィンドウを作成
205 GLFWwindow *window = glfwCreateWindow(800, 600, "GLSLシェーダープレイグラウンド", NULL, NULL); 420 GLFWwindow *window = glfwCreateWindow(800, 600, "GLSLシェーダー:頂点シェーダーと3D", NULL, NULL);
206 if (!window) { 421 if (!window) {
207 puts("GLFWウィンドウの作成に失敗しました"); 422 puts("GLFWウィンドウの作成に失敗しました");
208 glfwTerminate(); 423 glfwTerminate();
@@ -224,44 +439,31 @@ int main(int argc, char **argv) {
224 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); 439 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
225   440  
226 // シェーダーを読み込む 441 // シェーダーを読み込む
227 const char *vertexPath = "shader/vertex.glsl"; 442 if (loadPrograms() > 0) {
228 const char *fragmentPath = "shader/playground.glsl";    
229 GLuint shaderProgram = buildProgram(vertexPath, fragmentPath);    
230 if (!shaderProgram) {    
231 puts("シェーダーの読み込みに失敗しました"); 443 puts("シェーダーの読み込みに失敗しました");
232 glfwDestroyWindow(window); 444 glfwDestroyWindow(window);
233 glfwTerminate(); 445 glfwTerminate();
234 return -1; 446 return -1;
235 } 447 }
236   448  
237 // 頂点データ(フルスクリーンクアッド)を設定 449 // メッシュを作る
238 float vertices[] = { 450 Mesh cube = createCube();
239 -1.0f, -1.0f, 0.0f, 0.0f, // 左下(位置、UV) 451 Mesh sphere = createSphere(16, 12);
240 1.0f, -1.0f, 1.0f, 0.0f, // 右下 452 Mesh flag = createGrid(32, 20, 1.4f, 0.9f);
241 1.0f, 1.0f, 1.0f, 1.0f, // 右上    
242 -1.0f, 1.0f, 0.0f, 1.0f // 左上    
243 };    
244 GLuint indices[] = { 0, 1, 2, 2, 3, 0 };    
245      
246 GLuint VAO, VBO, EBO;    
247 glGenVertexArrays(1, &VAO);    
248 glGenBuffers(1, &VBO);    
249 glGenBuffers(1, &EBO);    
250   453  
251 glBindVertexArray(VAO); 454 // 頂点バッファなしで描く為の空のVAO(コアプロファイルではVAOが必須)
252 glBindBuffer(GL_ARRAY_BUFFER, VBO); 455 GLuint emptyVAO;
253 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 456 glGenVertexArrays(1, &emptyVAO);
254 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);    
255 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);    
256   457  
257 // 頂点属性:位置(location 0)、UV(location 1) 458 // 深度テストと裏面カリング
258 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0); 459 glEnable(GL_DEPTH_TEST);
259 glEnableVertexAttribArray(0); 460 glEnable(GL_CULL_FACE);
260 size_t offset = 2 * sizeof(float); 461 glCullFace(GL_BACK);
261 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)offset); 462 glFrontFace(GL_CCW);
262 glEnableVertexAttribArray(1);    
263   463  
264 int reloadWasDown = 0; 464 int reloadWasDown = 0;
    465 int gouraudWasDown = 0;
    466 int useGouraud = 0;
265   467  
266 // メインレンダリングループ 468 // メインレンダリングループ
267 while (!glfwWindowShouldClose(window)) { 469 while (!glfwWindowShouldClose(window)) {
@@ -271,40 +473,72 @@ int main(int argc, char **argv) {
271 // Rキーでシェーダーを再読み込み(失敗したら古いシェーダーのまま) 473 // Rキーでシェーダーを再読み込み(失敗したら古いシェーダーのまま)
272 int reloadDown = glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS; 474 int reloadDown = glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS;
273 if (reloadDown && !reloadWasDown) { 475 if (reloadDown && !reloadWasDown) {
274 GLuint newProgram = buildProgram(vertexPath, fragmentPath); 476 if (loadPrograms() == 0) puts("シェーダーを再読み込みしました");
275 if (newProgram) { 477 else puts("再読み込みに失敗しました。前のシェーダーを使い続けます");
276 glDeleteProgram(shaderProgram);    
277 shaderProgram = newProgram;    
278 puts("シェーダーを再読み込みしました");    
279 } else {    
280 puts("再読み込みに失敗しました。前のシェーダーを使い続けます");    
281 }    
282 } 478 }
283 reloadWasDown = reloadDown; 479 reloadWasDown = reloadDown;
284   480  
285 // 画面をクリア 481 // Gキーで頂点ごとのライティング(グーロー)とピクセルごと(フォン)を切り替える
286 glClearColor(0.0f, 0.0f, 0.0f, 1.0f); 482 int gouraudDown = glfwGetKey(window, GLFW_KEY_G) == GLFW_PRESS;
287 glClear(GL_COLOR_BUFFER_BIT); 483 if (gouraudDown && !gouraudWasDown) {
288   484 useGouraud = !useGouraud;
289 // シェーダープログラムを使用 485 puts(useGouraud ? "頂点ごとのライティング(グーロー)" : "ピクセルごとのライティング(フォン)");
290 glUseProgram(shaderProgram); 486 }
291   487 gouraudWasDown = gouraudDown;
292 // マウスの位置(ピクセル、左下が原点になる様にyを反転)    
293 double mouseX, mouseY;    
294 glfwGetCursorPos(window, &mouseX, &mouseY);    
295 int windowW, windowH;    
296 glfwGetWindowSize(window, &windowW, &windowH);    
297 float scale = windowW > 0 ? (float)gWidth / (float)windowW : 1.0f;    
298   488  
299 // ユニフォームを設定 489 float time = (float)glfwGetTime();
300 glUniform1f(glGetUniformLocation(shaderProgram, "uTime"), (float)glfwGetTime());    
301 glUniform2f(glGetUniformLocation(shaderProgram, "uResolution"), (float)gWidth, (float)gHeight);    
302 glUniform2f(glGetUniformLocation(shaderProgram, "uMouse"),    
303 (float)mouseX * scale, (float)(windowH - mouseY) * scale);    
304   490  
305 // クアッドを描画 491 // 画面と深度バッファをクリア
306 glBindVertexArray(VAO); 492 glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
307 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); 493 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    494  
    495 // 背景:頂点バッファなしのフルスクリーン三角形(深度テストなし)
    496 glDisable(GL_DEPTH_TEST);
    497 GLuint sky = gPrograms[PROGRAM_SKY].id;
    498 glUseProgram(sky);
    499 glUniform2f(glGetUniformLocation(sky, "uResolution"), (float)gWidth, (float)gHeight);
    500 glBindVertexArray(emptyVAO);
    501 glDrawArrays(GL_TRIANGLES, 0, 3);
    502 glEnable(GL_DEPTH_TEST);
    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 float aspect = gHeight > 0 ? (float)gWidth / (float)gHeight : 1.0f;
    510 Matrix4 proj = mat4Perspective(50.0f * PI / 180.0f, aspect, 0.1f, 100.0f);
    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);
308   542  
309 // バッファをスワップし、イベントを処理 543 // バッファをスワップし、イベントを処理
310 glfwSwapBuffers(window); 544 glfwSwapBuffers(window);
@@ -312,10 +546,11 @@ int main(int argc, char **argv) {
312 } 546 }
313   547  
314 // クリーンアップ 548 // クリーンアップ
315 glDeleteVertexArrays(1, &VAO); 549 destroyMesh(&cube);
316 glDeleteBuffers(1, &VBO); 550 destroyMesh(&sphere);
317 glDeleteBuffers(1, &EBO); 551 destroyMesh(&flag);
318 glDeleteProgram(shaderProgram); 552 glDeleteVertexArrays(1, &emptyVAO);
    553 for (int i = 0; i < PROGRAM_COUNT; i++) glDeleteProgram(gPrograms[i].id);
319 glfwDestroyWindow(window); 554 glfwDestroyWindow(window);
320 glfwTerminate(); 555 glfwTerminate();
321   556  

ファイル: /dev/null | b/shader/frag_gouraud.glsl

前新
@@ -0,0 +1,8 @@
    1 #version 450 core
    2  
    3 in vec3 vColor; // ラスタライザーが三角形の中で補間した色
    4 out vec4 out_color;
    5  
    6 void main() {
    7 out_color = vec4(pow(vColor, vec3(1.0 / 2.2)), 1.0);
    8 }

ファイル: /dev/null | b/shader/frag_phong.glsl

前新
@@ -0,0 +1,43 @@
    1 #version 450 core
    2  
    3 in vec3 vWorldPos;
    4 in vec3 vNormal;
    5 in vec2 vUv;
    6 out vec4 out_color;
    7  
    8 uniform vec3 uColor; // 物体の色(リニア)
    9 uniform vec3 uLightDir; // 表面から光の方へ向かう方向(ワールド空間)
    10 uniform vec3 uCameraPos; // カメラの位置(ワールド空間)
    11 uniform int uPattern; // 0:単色、1:チェッカー、2:日の丸
    12  
    13 // UVから物体の色を決める
    14 vec3 albedo() {
    15 if (uPattern == 1) {
    16 vec2 cell = floor(vUv * 4.0);
    17 float checker = mod(cell.x + cell.y, 2.0);
    18 return uColor * mix(0.55, 1.0, checker);
    19 }
    20 if (uPattern == 2) {
    21 vec2 p = (vUv - 0.5) * vec2(1.4 / 0.9, 1.0); // 旗の縦横比
    22 float disc = 1.0 - smoothstep(0.29, 0.31, length(p));
    23 return mix(uColor, vec3(0.75, 0.02, 0.05), disc);
    24 }
    25 return uColor;
    26 }
    27  
    28 void main() {
    29 vec3 n = normalize(vNormal);
    30 if (!gl_FrontFacing) n = -n; // 裏面(旗の裏側)では法線を反転
    31 vec3 l = normalize(uLightDir);
    32 vec3 v = normalize(uCameraPos - vWorldPos);
    33 vec3 h = normalize(l + v); // ハーフベクトル(Blinn-Phong)
    34  
    35 float diffuse = max(dot(n, l), 0.0);
    36 float specular = diffuse > 0.0 ? pow(max(dot(n, h), 0.0), 64.0) : 0.0;
    37  
    38 // 空と地面の色の半球ライト(アンビエント)
    39 vec3 ambient = mix(vec3(0.05, 0.04, 0.03), vec3(0.10, 0.13, 0.20), n.y * 0.5 + 0.5);
    40  
    41 vec3 color = albedo() * (ambient + diffuse) + vec3(0.6) * specular;
    42 out_color = vec4(pow(color, vec3(1.0 / 2.2)), 1.0); // ガンマ補正
    43 }

ファイル: /dev/null | b/shader/frag_sky.glsl

前新
@@ -0,0 +1,14 @@
    1 #version 450 core
    2  
    3 in vec2 vUv;
    4 out vec4 out_color;
    5  
    6 uniform vec2 uResolution;
    7  
    8 void main() {
    9 float t = gl_FragCoord.y / uResolution.y; // 0(下)〜1(上)
    10 vec3 horizon = vec3(0.32, 0.30, 0.36);
    11 vec3 zenith = vec3(0.07, 0.10, 0.20);
    12 vec3 color = mix(horizon, zenith, smoothstep(0.0, 1.0, t));
    13 out_color = vec4(color, 1.0);
    14 }

ファイル: /dev/null | b/shader/vertex_flag.glsl

前新
@@ -0,0 +1,41 @@
    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 out vec3 vWorldPos;
    8 out vec3 vNormal;
    9 out vec2 vUv;
    10  
    11 uniform mat4 uModel;
    12 uniform mat4 uView;
    13 uniform mat4 uProj;
    14 uniform mat3 uNormalMatrix;
    15 uniform float uTime;
    16  
    17 const float AMPLITUDE = 0.15; // 波の高さ(旗の端で)
    18 const float WAVE_K = 5.0; // 波数(x方向に1あたり何ラジアン)
    19 const float WAVE_Y = 1.5; // 斜めの波にする為のy方向の波数
    20 const float SPEED = 4.0; // 角速度(ラジアン/秒)
    21 const float WIDTH = 1.4; // 旗の幅(C側の格子と同じ)
    22  
    23 void main() {
    24 vec3 p = aPosition;
    25  
    26 // 竿(x = 0)では動かず、端に行く程大きく揺れる
    27 float a = AMPLITUDE * p.x / WIDTH;
    28 float phase = WAVE_K * p.x + WAVE_Y * p.y - SPEED * uTime;
    29 p.z = a * sin(phase);
    30  
    31 // 法線:曲面 z = h(x, y) の偏微分から (-dh/dx, -dh/dy, 1)
    32 float dhdx = AMPLITUDE / WIDTH * sin(phase) + a * WAVE_K * cos(phase);
    33 float dhdy = a * WAVE_Y * cos(phase);
    34 vec3 normal = normalize(vec3(-dhdx, -dhdy, 1.0));
    35  
    36 vec4 worldPos = uModel * vec4(p, 1.0);
    37 vWorldPos = worldPos.xyz;
    38 vNormal = uNormalMatrix * normal;
    39 vUv = aUv;
    40 gl_Position = uProj * (uView * worldPos);
    41 }

ファイル: /dev/null | b/shader/vertex_fullscreen.glsl

前新
@@ -0,0 +1,11 @@
    1 #version 450 core
    2  
    3 out vec2 vUv;
    4  
    5 // 頂点バッファなしで画面全体を覆う大きな三角形
    6 // gl_VertexID = 0, 1, 2 → (-1, -1), (3, -1), (-1, 3)
    7 void main() {
    8 vec2 uv = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
    9 vUv = uv;
    10 gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0);
    11 }

ファイル: /dev/null | b/shader/vertex_gouraud.glsl

前新
@@ -0,0 +1,30 @@
    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 out vec3 vColor; // 頂点で計算したライティングの結果
    8  
    9 uniform mat4 uModel;
    10 uniform mat4 uView;
    11 uniform mat4 uProj;
    12 uniform mat3 uNormalMatrix;
    13 uniform vec3 uColor;
    14 uniform vec3 uLightDir;
    15 uniform vec3 uCameraPos;
    16  
    17 void main() {
    18 vec4 worldPos = uModel * vec4(aPosition, 1.0);
    19 vec3 n = normalize(uNormalMatrix * aNormal);
    20 vec3 l = normalize(uLightDir);
    21 vec3 v = normalize(uCameraPos - worldPos.xyz);
    22 vec3 h = normalize(l + v);
    23  
    24 float diffuse = max(dot(n, l), 0.0);
    25 float specular = diffuse > 0.0 ? pow(max(dot(n, h), 0.0), 64.0) : 0.0;
    26 vec3 ambient = mix(vec3(0.05, 0.04, 0.03), vec3(0.10, 0.13, 0.20), n.y * 0.5 + 0.5);
    27  
    28 vColor = uColor * (ambient + diffuse) + vec3(0.6) * specular;
    29 gl_Position = uProj * (uView * worldPos);
    30 }

ファイル: /dev/null | b/shader/vertex_mesh.glsl

前新
@@ -0,0 +1,22 @@
    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 out vec3 vWorldPos; // ワールド空間の位置
    8 out vec3 vNormal; // ワールド空間の法線
    9 out vec2 vUv;
    10  
    11 uniform mat4 uModel; // オブジェクト空間 → ワールド空間
    12 uniform mat4 uView; // ワールド空間 → ビュー空間
    13 uniform mat4 uProj; // ビュー空間 → クリップ空間
    14 uniform mat3 uNormalMatrix; // 法線用(モデル行列の逆行列の転置)
    15  
    16 void main() {
    17 vec4 worldPos = uModel * vec4(aPosition, 1.0);
    18 vWorldPos = worldPos.xyz;
    19 vNormal = uNormalMatrix * aNormal;
    20 vUv = aUv;
    21 gl_Position = uProj * (uView * worldPos);
    22 }


コメント(0)

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