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が行います。
で割り(第12章の透視除算)、glViewportで設定したビューポートでNDCをピクセルに変換します。
オブジェクト空間の位置にある頂点を数式1行で書くと:
はモデル行列(オブジェクト → ワールド)、はビュー行列(ワールド → ビュー)、は投影行列(ビュー → クリップ)です。
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章の通常の物です。
行列の要素は、の行目との列目の内積です。
// 行列の積 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用透視投影行列です。
、縦横比、ニア面、ファー面とすると:
// 透視投影(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を渡すか、自分で転置します。
此処を間違えてもクラッシュはしません。
平行移動が一番下の行に入ってに掛かってしまうので、物体が変な方向に飛んで行くだけです。
計算例:1つの頂点を画面まで
数字が魔法に見えない様に、立方体の角を1つ手で追ってみましょう。
立方体の角はオブジェクト空間で、モデル行列は立方体をだけ左上に動かすだけ、そして(数字を簡単にする為に)カメラはから原点を見ているとします。
其の場合、ビュー行列はの平行移動だけです。
投影はfovY = 50°、、、です:
ワールド空間:。
ビュー空間:。
つまりカメラの4.5単位前にあります(前はzが負の方向)。
クリップ空間を1行ずつ:
此れが頂点シェーダーがgl_Positionに書く値です。
次にGPUがで割ります。
NDC 。
最後にビューポートがをピクセルに変換します。
、(ウィンドウの下から)。
の0.9575は深度バッファに入り、直ぐ後で必要になります。
実際のプログラムではカメラはにあり、少し下のを見ているので、ビュー行列は少し綺麗ではなくなりますが、手順は同じです。
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つの角はになり、一辺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の面ではです。
此の規則があると、角は外から見て反時計回りに並びます。
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の位置をの同次座標の点にします。
方向なら平行移動の影響を受けない様ににします。
此の考え方は直ぐに法線で使います。
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〜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枚目の画像です。
計算例のの0.9575を覚えていますか?
GPUはNDCのを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は緑、+Xならピンクがかった赤、左の面の−Xは青緑です。
13.5 法線と法線行列
ライティングには、光の方向やカメラの位置と同じく、ワールド空間の法線が必要です。
すぐ思い付くのはモデル行列を掛ける事です。
mat3(uModel) * aNormal(mat3(...)コンストラクターはmat4の左上3×3を取り出し、方向が無視すべき平行移動を落とします)。
回転と均一な拡大縮小なら此れで動きますが、軸毎に違う拡大縮小をすると直ぐに壊れます:
式
法線の正しい行列は、モデル行列の左上3×3の部分の逆行列の転置です:
何故此れで正しいか
法線は1つの性質で定義されます。
表面の全ての接ベクトルに垂直である事、です。
接ベクトルは表面上の近い点同士の差なので、位置と同じ様に変換されます。
。
新しい法線が新しい接ベクトルにまだ垂直になる様な行列が欲しいです:
なら、此れは全ての接ベクトルについてになり、正に欲しい物です。
従って、つまりです(逆行列は線型代数の第10章、転置は第7章)。
2つの特別な場合が、素朴なやり方でも大抵上手く行く様に見える理由を説明します:
- 純粋な回転はなので。法線行列は回転其の物です。
- 倍の均一な拡大縮小ではになり、長さが変わるだけで、どうせ正規化します。
計算例
あたし達の球はで潰されています。
単位球上の点を取ります。
其の法線は同じベクトルで、接ベクトルの1つはです(確認:)。
潰した後、接ベクトルはになります。
素朴な法線はで:
従って、もう表面に垂直ではありません。
対角行列の逆行列の転置は、対角の逆数を取るだけです。
なので、正しい法線はで:
画面上の違いは此の通りです(ライティングのシェーダーは次の節です):
上の物は、丸い球の陰影を平らな形に描いた様に見えます。
ハイライトは高過ぎる位置にあり、下側もまだ照らされています。
下の物は本当の形、平たい小石の様に見えます。
光は広い上面に当たり、下側は暗いです。
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章)。
此の関数を、適当な回転・拡大縮小・平行移動の行列で数値的な逆行列の転置と比べました。
最大の差は未満でした。
最終版の頂点シェーダーは、ワールド空間の位置、法線、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球です。
スタック、スライスの角度はとで、点はです。
単位球では法線は位置其の物なので、法線はタダで手に入ります。
コードは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平面上にあり、左端が(竿がある所)です。
頂点シェーダーが扱う物が見える様に、シーン全体をワイヤーフレームで表示すると(glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)):
波
旗に沿って進むサイン波で各頂点を方向に押し、振幅を竿の0から自由端(幅)のまで大きくします:
は方向に1単位あたり波が何ラジアン進むか、は波面が完全に垂直にならない様に少し傾け、はラジアン毎秒の速さです。
法線
頂点を動かすと表面が変わるので、古い法線は間違いになります。
幸い、表面は正確に分かっています。
です。
2つの接ベクトルは偏微分とで(はの意味)、其の外積が法線です:
積の微分法則を使うと(振幅もに依存するので):
#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) |
画面の外の部分はタダでクリッピングされ、画面の中では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つの重みを持ち、頂点シェーダーがボーンの行列を混ぜます。。ゲームで見たアニメーションするキャラクターは全て此の仕組みです。
- 風と水:草、葉、旗は頂点シェーダーのサイン波で揺れ、普通は頂点カラーに塗った値で重み付けします(あたし達の竿側の端の様に根元は動きません)。海面は幾つもの動く波を重ねます(ゲルストナー波がよく使われます)。
- インスタンシング:
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 課題
- ★ モデル行列が倍に拡大縮小します。接ベクトルを持つ表面の法線を、モデル行列と法線行列で其々変換し、どちらが変換後の接ベクトルに垂直か確かめて下さい(片方は内積が3、もう片方は0になるはずです)。
- ★ 13.2の計算例で、立方体の角をに変え、NDCのとピクセルのを手で計算して下さい(左端から約85ピクセルの所になるはずです)。
- ★★ ウィンドウを作る前に
glfwWindowHint(GLFW_SAMPLES, 4);を、GLADを読み込んだ後にglEnable(GL_MULTISAMPLE);を追加して下さい。立方体と竿のギザギザの縁が滑らかになるはずです(マルチサンプルアンチエイリアス)。 - ★★ マウスでカメラを回せる様にして下さい。
mouseX / windowWから角度を計算し、目をに置きます。マウスを左右に動かすとシーンの周りを回り、ライティングはカメラではなく物体に付いたままになるはずです。 - ★★ 旗に、違う
WAVE_KとSPEEDの小さな2つ目の波を加え、其の微分をdhdxとdhdyに足して下さい。一時的にn * 0.5 + 0.5を色として出力して法線を確認します。継ぎ目なく滑らかに変化するはずです。 - ★★ 球を
createSphere(64, 48)に変えてGキーを押して下さい。グーロー版とフォン版が殆ど同じに見えるはずです。昔はハイポリのモデルがグーローの弱点を隠していた理由です。 - ★★★ 10 × 10個の立方体を、
glDrawElementsInstancedの1回の呼び出しで描いて下さい。頂点シェーダーでgl_InstanceIDからオフセットを計算し(vec3(id % 10, 0, id / 10)を拡大して中心に寄せる)、ワールド空間の位置に足します。1回の描画呼び出しで100個の立方体が見えるはずです。
ファイル: /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)
まだコメントはありません。
ログインするとコメント出来ます。