12. 法線マッピング
ゲームのレンガの壁は、大抵たった2つの三角形です。
其れなのに光が横切ると、全てのレンガの縁に小さな陰が出来て、目地はレンガより奥にある様に見えます。
其の細部は形状としては存在しません。
色ではなく法線を保存したテクスチャから来ています。
此の章では、そんな法線マップをCで高さマップから生成し、其れで平らな壁を照らし、シェーダーの中で高さから直接法線を計算し、最後にレンガを球に貼ります。
球には接線空間とTBN行列が必要になります。
第11章のテクスチャと、第9章・第10章のライティングの上に作ります。
12.1 考え方:形状無しで細部を
此れ迄のライティングは全て、表面の1つの事だけに依存していました。
其の法線です。
ランバートの拡散反射は(線型代数第4章)、Blinn-Phongの鏡面反射はです。
ライティングは表面が実際に何処にあるかは聞かず、どちらを向いているかだけを聞きます。
そこで此の技です。
表面は平らなままにして、ピクセル毎に別の法線をライティングに渡します。
表面が凸凹だったとしたら持っていた筈の法線です。
どちらの面もライティングの式に同じ法線を渡すので、同じ様に照らされます。
平らな方は、何千もの三角形ではなく2つの三角形で済みます。
法線マッピングに出来ない事もあります。
物体の輪郭は変えられません(法線マップを貼った球も、縁は完全に丸いままです)。
凸凹同士が本物の影を落とし合う事もありません。
でも表面の内側の光と陰については非常に説得力があり、2000年代半ば以降の殆ど全ての3Dゲームが、殆ど全ての表面で使っています。
12.2 高さマップから法線へ
凸凹を表す一番簡単な方法は高さマップです。
黒が低く(0)、白が高い(1)灰色のテクスチャです。
此処から全てのテクセルの法線を計算出来ます。
式
テクスチャを地形だと思って下さい。
テクセルの表面の点はです。
が高さマップで、は強さ、つまり高さ1が何テクセル分の深さかです。
法線は次の通りです:
傾きは中心差分で測ります。
右の隣の高さから左の隣の高さを引き、其の間の2テクセルで割ります:
は「のについての偏微分」と読み、単に「だけを動かした時にがどれだけ速く変わるか」という意味です。
仕組み
地形の上を1テクセル右に歩くと、は1増え、高さは増えます。
従って表面はの方向に進みます。
1テクセル上に歩くとです。
どちらの矢印も表面の中にあり、外積(線型代数第5章)で両方に垂直なベクトルが得られます。
、と書くと:
此れが正規化する前の式です。
横から見ると簡単に確かめられます。
地面が右に上がっている所では、法線は左に傾きます。
平らな所では両方の傾きが0なので、、表面から真っ直ぐ外向きです。
計算例
レンガの左の縁の、右に向かって高さが上がっているテクセルを考えます。
左の隣は、右の隣は、上と下の隣は同じ高さです。
強さなら:
図と同じ様に、法線は上り坂とは反対の左に傾いています。
法線をテクスチャに保存する
単位法線の成分は−1から1の間ですが、GL_RGBA8のテクスチャは0から1を保存します。
其の為、書く時に詰め込み、読む時に広げます:
例の法線は、バイトにすると(255を掛けて四捨五入)になります。
平らな法線は、バイトではで、明るい青紫色です。
法線マップの殆どの部分は大体真っ直ぐ外を向いているので、法線マップはいつも青っぽく見えます。
戻すと正確な0ではなくになります(8ビットでは127.5を保存出来ません)。
読んだ後に必ずnormalizeする理由の1つです。
12.3 Cでマップを作る
第11章の上に、レンガのテクスチャをmain.cに追加しましょう。
先ず、第11章のcreateTestTextureがループの後にしていた事をする補助関数です。
テクスチャ毎に同じ事を書かずに済みます:
// ピクセルデータからテクスチャを作る(TEX_SIZE×TEX_SIZE、ミップマップ付き、GL_REPEAT)
GLuint uploadTexture(GLint internalFormat, GLenum format, const unsigned char *pixels) {
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, TEX_SIZE, TEX_SIZE, 0,
format, GL_UNSIGNED_BYTE, pixels);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
return texture;
}
高さマップ
レンガは64 × 32テクセルで、1段置きに半分ずらします。
テクセル毎にレンガの一番近い縁迄の距離を求め、2テクセル以内なら目地(高さ0)、其処から4テクセルかけてレンガの上面(高さ1)迄滑らかに上がります。
少しの値ノイズ(第8章と同じ考え方をCで書いた物)で、レンガの表面を少しザラザラにします:
// GLSLのsmoothstepと同じ(0〜1の滑らかな段差)
float smoothstepf(float edge0, float edge1, float x) {
float t = (x - edge0) / (edge1 - edge0);
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
return t * t * (3.0f - 2.0f * t);
}
// 整数の座標から0〜1の疑似乱数を作るハッシュ(第8章と同じ考え方)
float hash2(int x, int y) {
unsigned int h = (unsigned int)x * 374761393u + (unsigned int)y * 668265263u;
h = (h ^ (h >> 13)) * 1274126177u;
return (float)((h ^ (h >> 16)) & 0xffffu) / 65535.0f;
}
// cellテクセル毎の値ノイズ(TEX_SIZEで繰り返すので、テクスチャの継ぎ目が見えない)
float valueNoise(int x, int y, int cell) {
int n = TEX_SIZE / cell;
int cx = x / cell, cy = y / cell;
float fx = smoothstepf(0.0f, 1.0f, (float)(x % cell) / cell);
float fy = smoothstepf(0.0f, 1.0f, (float)(y % cell) / cell);
float a = hash2(cx % n, cy % n);
float b = hash2((cx + 1) % n, cy % n);
float c = hash2(cx % n, (cy + 1) % n);
float d = hash2((cx + 1) % n, (cy + 1) % n);
return (a + (b - a) * fx) * (1.0f - fy) + (c + (d - c) * fx) * fy;
}
// レンガの高さマップ(0〜1)を作る:レンガは高く、目地は低く、レンガの縁は斜め
float *createBrickHeights(void) {
float *heights = (float *)malloc(TEX_SIZE * TEX_SIZE * sizeof(float));
if (!heights) {
puts("高さマップのメモリ確保に失敗しました");
return NULL;
}
for (int y = 0; y < TEX_SIZE; y++) {
for (int x = 0; x < TEX_SIZE; x++) {
// レンガは64×32テクセル、1段毎に半分ずらす
int row = y / 32;
int bx = (x + (row % 2) * 32) % 64;
int by = y % 32;
// レンガの一番近い縁までの距離(テクセル)
float ex = fminf(bx + 0.5f, 64.0f - (bx + 0.5f));
float ey = fminf(by + 0.5f, 32.0f - (by + 0.5f));
float e = fminf(ex, ey);
// 縁から2テクセルは目地(高さ0)、其処から4テクセルかけて斜めに上がる
float h = smoothstepf(2.0f, 6.0f, e);
// レンガの表面の細かい凸凹
float rough = 0.6f * valueNoise(x, y, 8) + 0.4f * valueNoise(x, y, 4);
h *= 0.8f + 0.2f * rough;
heights[y * TEX_SIZE + x] = h;
}
}
return heights;
}
valueNoiseの% nでノイズは256テクセル毎に回り込み、256はレンガの大きさの倍数でもあるので、テクスチャは目に見える継ぎ目無く繰り返します。
GL_REPEATを使うので此れは大事です。
1つの高さマップから2つのテクスチャ
高さ其の物は1チャンネルのテクスチャとして、其処から計算した法線(12.2節)はRGBAのテクスチャとしてアップロードします:
// 高さマップをそのまま1チャンネルのテクスチャにする(GL_R8)
GLuint createHeightTexture(const float *heights) {
if (!heights) return 0;
unsigned char *pixels = (unsigned char *)malloc(TEX_SIZE * TEX_SIZE);
if (!pixels) return 0;
for (int i = 0; i < TEX_SIZE * TEX_SIZE; i++) {
pixels[i] = (unsigned char)(heights[i] * 255.0f + 0.5f);
}
GLuint texture = uploadTexture(GL_R8, GL_RED, pixels);
free(pixels);
return texture;
}
// 高さマップから法線マップを作る(中心差分、OpenGLの「緑が上」の向き)
GLuint createNormalTexture(const float *heights, float strength) {
if (!heights) return 0;
unsigned char *pixels = (unsigned char *)malloc(TEX_SIZE * TEX_SIZE * 4);
if (!pixels) return 0;
for (int y = 0; y < TEX_SIZE; y++) {
for (int x = 0; x < TEX_SIZE; x++) {
// 隣のテクセル(端では反対側に回り込む、GL_REPEATと同じ)
int xl = (x + TEX_SIZE - 1) % TEX_SIZE, xr = (x + 1) % TEX_SIZE;
int yd = (y + TEX_SIZE - 1) % TEX_SIZE, yu = (y + 1) % TEX_SIZE;
// 1テクセル進んだ時の高さの変化
float dhdx = (heights[y * TEX_SIZE + xr] - heights[y * TEX_SIZE + xl]) * 0.5f;
float dhdy = (heights[yu * TEX_SIZE + x] - heights[yd * TEX_SIZE + x]) * 0.5f;
// 法線 = (-s・dh/dx, -s・dh/dy, 1) を正規化
float nx = -strength * dhdx;
float ny = -strength * dhdy;
float nz = 1.0f;
float len = sqrtf(nx * nx + ny * ny + nz * nz);
nx /= len;
ny /= len;
nz /= len;
// -1〜1を0〜255に詰める
unsigned char *p = &pixels[(y * TEX_SIZE + x) * 4];
p[0] = (unsigned char)((nx * 0.5f + 0.5f) * 255.0f + 0.5f);
p[1] = (unsigned char)((ny * 0.5f + 0.5f) * 255.0f + 0.5f);
p[2] = (unsigned char)((nz * 0.5f + 0.5f) * 255.0f + 0.5f);
p[3] = 255;
}
}
GLuint texture = uploadTexture(GL_RGBA8, GL_RGBA, pixels);
free(pixels);
return texture;
}
GL_REDのGL_R8は1テクセル1バイトで保存します。
シェーダーでサンプリングするとが返るので、高さは.rで読みます。
各行は256バイトで4の倍数なので、第11章のアンパックアラインメントも問題ありません。
3つのテクスチャをバインドする
main()のテスト用テクスチャの後で:
// レンガの法線マップと高さマップを作る(第12章)
float *heights = createBrickHeights();
GLuint normalMap = createNormalTexture(heights, 3.0f);
GLuint heightMap = createHeightTexture(heights);
free(heights);
強さ3は、高さの差1が3テクセル分の深さという意味なので、4テクセルの斜面はかなり急です。
レンダリングループでは、ユニット0のテスト用テクスチャの後で、新しい2つのテクスチャをユニット1と2に入れます:
// 法線マップはユニット1、高さマップはユニット2
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, normalMap);
glUniform1i(glGetUniformLocation(shaderProgram, "uNormalMap"), 1);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, heightMap);
glUniform1i(glGetUniformLocation(shaderProgram, "uHeightMap"), 2);
此処に1つ罠があります。
アクティブなテクスチャユニットはグローバルな状態で、此のコードの後は誰かが変える迄ユニット2のままです。
次のフレームの最初で、第11章のコードがFキーとWキーの為にglBindTexture(GL_TEXTURE_2D, texture)を呼ぶと、テスト用テクスチャがユニット2に入ってしまい、setFilterも其処で変更してしまいます。
其の為、先にユニット0に戻します:
// Fキーでフィルタリング、Wキーでラップモードを切り替える
// (アクティブなユニットは前のフレームの最後のままなので、ユニット0に戻す)
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
最後に、他の後片付けの隣にglDeleteTextures(1, &normalMap);とglDeleteTextures(1, &heightMap);を追加します。
マップを確かめる為に、次のシェーダーで左に高さマップ、右に法線マップの色をそのまま表示します:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform vec2 uResolution;
uniform sampler2D uNormalMap; // ユニット1
uniform sampler2D uHeightMap; // ユニット2
void main() {
// 画面の左半分と右半分の其々に、縦横比を直したUVを作る
float halfWidth = uResolution.x * 0.5;
vec2 local = vec2(mod(gl_FragCoord.x, halfWidth), gl_FragCoord.y);
vec2 uv = (local - vec2(halfWidth, uResolution.y) * 0.5) / uResolution.y + 0.5;
float height = texture(uHeightMap, uv).r; // GL_R8は赤チャンネルだけ
vec3 normalColor = texture(uNormalMap, uv).rgb;
// 左半分:高さマップ、右半分:法線マップ
vec3 color = gl_FragCoord.x < halfWidth ? vec3(height) : normalColor;
out_color = vec4(color, 1.0);
}
(変数名をhalfWidthにしたのは、halfがGLSLの予約語だからです。)
法線マップは次の様に読みます。
平らな所はの青、レンガの右の縁はピンク寄り(赤が多く、法線がに傾く)、左の縁は濃い青(赤が少なく、に傾く)、上の縁は水色(緑が多く、上に傾く)、下の縁は紫(緑が少なく、下に傾く)です。
12.4 照らされた壁
では壁を照らしましょう。
光はマウスの位置の、壁から0.5手前にあり、目はから真っ直ぐ壁を見ています:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform sampler2D uNormalMap;
uniform sampler2D uHeightMap;
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec2 uv = p * 0.5 + 0.5;
// 壁の上の点と、マウスの位置にある光(壁から0.5手前)
vec3 pos = vec3(p, 0.0);
vec2 mouse = (2.0 * uMouse - uResolution) / uResolution.y;
vec3 lightPos = vec3(mouse, 0.5);
vec3 L = normalize(lightPos - pos); // 光への方向
vec3 V = vec3(0.0, 0.0, 1.0); // 目への方向
// 法線:今は只の平らな壁
vec3 N = vec3(0.0, 0.0, 1.0);
// 色:低い所(目地)は灰色、高い所(レンガ)は赤茶色
float h = texture(uHeightMap, uv).r;
vec3 albedo = mix(vec3(0.3, 0.28, 0.25), vec3(0.5, 0.2, 0.12), smoothstep(0.05, 0.4, h));
// Blinn-Phong(第9章)
float diffuse = max(dot(N, L), 0.0);
vec3 H = normalize(L + V);
float specular = pow(max(dot(N, H), 0.0), 32.0) * 0.25;
vec3 color = albedo * (0.05 + diffuse) + vec3(specular);
// ガンマ補正(第10章)
color = pow(color, vec3(1.0 / 2.2));
out_color = vec4(color, 1.0);
}
ライティングは第9章のBlinn-Phongモデルです。
アンビエント0.05、拡散反射、そしてハーフベクトルと光沢度32の鏡面反射です。
色は線型の値で、第10章で説明した様に最後にガンマ補正します。
此処では高さマップは色を決めるだけです。
正しく照らされていますが、明らかにレンガの絵です。
光は滑らかな明るい点を作るだけです。
では平らな法線を次の1行に置き換えます:
// 法線:法線マップから読み、0〜1を-1〜1に戻す
vec3 N = normalize(texture(uNormalMap, uv).rgb * 2.0 - 1.0);
レンガが壁から浮き出ました。
光の方(左上)を向いた斜面は明るく、反対を向いた斜面は暗く、各レンガの表面は少しザラザラで、ハイライトは凸凹で崩れます。
マウスを動かすと光と陰も一緒に動きます。
描いただけのテクスチャには出来ない事です。
何故法線マップのベクトルをそのまま使えたのでしょうか?
法線マップは、テクスチャ自身の座標系で法線を保存しているからです。
其処ではがの増える方向、がの増える方向、が表面から外向きです。
此の壁では其れが偶然画面の、、と丁度同じなので、変換が要りません。
球では其れが成り立たなくなるので、12.6節で扱います。
12.5 シェーダーで高さマップから法線を計算する
法線はCで焼き込みましたが、シェーダーでも計算出来ます。
Cの法線マップと並べて、2つの方法を見てみましょう:
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform sampler2D uNormalMap;
uniform sampler2D uHeightMap;
// 高さマップから法線を計算する(Cのコードと同じ中心差分)
vec3 normalFromHeight(sampler2D heightMap, vec2 uv, float strength) {
vec2 texel = 1.0 / vec2(textureSize(heightMap, 0)); // 1テクセルのUVでの大きさ
float hL = texture(heightMap, uv - vec2(texel.x, 0.0)).r; // 左
float hR = texture(heightMap, uv + vec2(texel.x, 0.0)).r; // 右
float hD = texture(heightMap, uv - vec2(0.0, texel.y)).r; // 下
float hU = texture(heightMap, uv + vec2(0.0, texel.y)).r; // 上
vec2 slope = vec2(hR - hL, hU - hD) * 0.5; // 1テクセル当たりの高さの変化
return normalize(vec3(-strength * slope, 1.0));
}
// 画面空間の微分(dFdx・dFdy)で法線を計算する
vec3 normalFromDerivatives(float h, vec2 uv, float strength) {
vec2 texelsPerPixel = vec2(dFdx(uv.x), dFdy(uv.y)) * vec2(textureSize(uHeightMap, 0));
vec2 slope = vec2(dFdx(h), dFdy(h)) / texelsPerPixel; // 1テクセル当たりの高さの変化
return normalize(vec3(-strength * slope, 1.0));
}
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec2 uv = p * 0.5 + 0.5;
vec3 pos = vec3(p, 0.0);
vec2 mouse = (2.0 * uMouse - uResolution) / uResolution.y;
vec3 L = normalize(vec3(mouse, 0.5) - pos);
vec3 V = vec3(0.0, 0.0, 1.0);
float h = texture(uHeightMap, uv).r;
// 3つの方法を比べる:左 = C で作った法線マップ、中 = 中心差分、右 = dFdx・dFdy
vec3 fromMap = normalize(texture(uNormalMap, uv).rgb * 2.0 - 1.0);
vec3 fromHeight = normalFromHeight(uHeightMap, uv, 3.0);
vec3 fromDerivatives = normalFromDerivatives(h, uv, 3.0);
float third = gl_FragCoord.x / uResolution.x * 3.0;
vec3 N = third < 1.0 ? fromMap : (third < 2.0 ? fromHeight : fromDerivatives);
vec3 albedo = mix(vec3(0.3, 0.28, 0.25), vec3(0.5, 0.2, 0.12), smoothstep(0.05, 0.4, h));
float diffuse = max(dot(N, L), 0.0);
vec3 H = normalize(L + V);
float specular = pow(max(dot(N, H), 0.0), 32.0) * 0.25;
vec3 color = albedo * (0.05 + diffuse) + vec3(specular);
color = pow(color, vec3(1.0 / 2.2));
if (abs(fract(third + 0.5) - 0.5) * uResolution.x / 3.0 < 1.0 && third > 0.5) color = vec3(0.0);
out_color = vec4(color, 1.0);
}
中心差分(normalFromHeight、真ん中)は、Cのコードをシェーダーに移した物です。
1テクセル左、右、下、上で4回余分にサンプリングします。
見た目は左の焼き込んだ法線マップと同じです。
1回ではなく4回のサンプリングが必要ですが、強さを実行中に変えられ、保存するのは1チャンネルの高さマップだけで済みます。
画面空間の微分(normalFromDerivatives、右)は、第11章のdFdxとdFdyを使います。
dFdx(h)は、此のピクセルから右の隣のピクセル迄に高さがどれだけ変わるかです。
1ピクセル進むと何テクセル進むか(dFdx(uv.x)にテクスチャの大きさを掛けた物)で割ると、「1テクセル当たりの高さの変化」になり、後は12.2節の式です。
余分なサンプリングが全く要りませんが、良く見て下さい:
微分版には階段状の段があり、表面もザラザラです。
GPUは微分を2 × 2ピクセルのブロック毎に1回計算するので(第11章)、ブロックの4つのピクセルが1つの傾きを共有するからです。
小さくて安い細部(デカール、プロシージャルな凸凹)には十分で、正に其の理由でシェーダーアートで人気があります。
又、此の簡単な版が動くのは、壁が画面と揃っていて、がに、がに沿っているからに過ぎません。
一般の表面では、dFdx(position)も使うMorten Mikkelsenの「微分によるバンプマッピング」の方法が必要です。
12.6 接線空間とTBN行列
法線マップは法線を接線空間で保存しています。
表面の各点に貼り付いた小さな座標系です:
- 、接線(タンジェント):表面の上でが増える方向。
- 、従接線(バイタンジェント):が増える方向。
- 、表面其の物の法線(マップ無しの、形状の法線)。
法線マップの値は「表面の法線其の物」、なら「寝かせての方向を向く」という意味です。
此れが法線マップを使い回せる理由です。
同じレンガのテクスチャが壁にも床にも樽にも球にも使えます。
表面に対して法線がどれだけ傾くかしか言っていないからです。
式
接線空間の法線をライティングで使うには、とがある空間(此処ではプレイグラウンドのビュー空間)に変換します。
其れは正に線型代数第11章の基底変換です。
古い座標で書いた新しい軸を、列として行列に入れます:
GLSLではmat3(T, B, N)(3つのvec3を引数にしたmat3コンストラクタは、其れらを列として使います)とTBN * nTangentです。
仕組み
「に沿って、に沿って、に沿って歩く」。
行列とベクトルの積は、其の列の正にそういう線型結合です。
平らな壁では、、で、TBN行列は単位行列です。
だから12.4節は此れ無しで動きました。
計算例です。
表面の法線が、接線が、従接線がの、球の上の点を考えます。
此れらは正規直交です。
どれも長さ1()で、です。
法線マップがに傾いたを返したとすると:
なので、まだ単位ベクトルです。
表面其の物よりも更に右に傾いています。
、、は正規直交なので、逆行列は転置です(線型代数第10章):。
多くのエンジンは其れを使って逆向きに変換します。
頂点シェーダーでとを接線空間に入れて(頂点毎に1回なので安い)、フラグメントシェーダーでは法線マップの値でそのままライティングします。
どちらでも結果は同じです。
12.7 法線マップを貼った球
次はレンガを球に貼ります。
第9章と同じ「球のインポスター」の考え方を使います。
円を描き、其の中で球の点を計算します。
原点にある半径1の球なら、単位円の中ののピクセルにはの点(から)が見え、単位球では位置が其のまま法線です。
球のUV
地球儀の経度と緯度の様に、全ての点にとが必要です:
- 引数が2つの
atan(y, x)は、点の角度をからで、正しい象限で返します。此処ではとを渡すので、角度は縦軸の周りを回ります。正面()で0、に向かって増えます。で割って0.5を足すと、球の周りで0〜1になります。 asin(x)は逆正弦で、からを返します。此れが緯度で、赤道で0、極で、は下の0から上の1迄です。
周りは、上から下はしかないので、赤道でレンガが歪まない様に、テクスチャを周りに4回、極から極に2回繰り返します。
に少しuTimeを足すと、球がゆっくり回ります。
球の接線
経度を、緯度をとすると、点はです。
接線はが増える方向、つまりが増える方向です:
、なので、此れは単に、球の周りを回る水平な方向です。
従接線はです。
確かめてみると、此の外積はが増える方向と等しくなるので、座標系は右手系で、はちゃんと球の上に向かいます。
丁度極ではで接線の方向が決まりません(極では全ての方向が「周り」です)。
其の為、コードは其処ではを使います。
シェーダー
#version 450 core
in vec2 vUv;
out vec4 out_color;
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform sampler2D uNormalMap;
uniform sampler2D uHeightMap;
const float PI = 3.14159265;
void main() {
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
// 半径0.85の球(第9章の球と同じ考え方:zは x² + y² + z² = 1 から)
vec2 q = p / 0.85;
float r2 = dot(q, q);
vec3 N = vec3(q, sqrt(max(1.0 - r2, 0.0))); // 半径1の球では位置 = 法線
vec3 pos = vec3(p, N.z * 0.85);
// 球面のUV:経度がu、緯度がv(uを時間でずらして自転させる)
vec2 uv;
uv.x = atan(N.x, N.z) / (2.0 * PI) + 0.5 + uTime * 0.03;
uv.y = asin(clamp(N.y, -1.0, 1.0)) / PI + 0.5;
uv *= vec2(4.0, 2.0); // 横に4回、縦に2回繰り返すとレンガが歪まない
// 接線空間の基底:Tはuが増える方向、Bはvが増える方向
vec3 T = vec3(N.z, 0.0, -N.x);
T = length(T) > 1e-4 ? normalize(T) : vec3(1.0, 0.0, 0.0); // 極ではTが決まらない
vec3 B = cross(N, T);
mat3 TBN = mat3(T, B, N); // 列がT、B、N
// 法線マップの法線(接線空間)をビュー空間に変換する
vec3 nTangent = texture(uNormalMap, uv).rgb * 2.0 - 1.0;
vec3 n = normalize(TBN * nTangent);
// 光はマウスの位置、球の手前1.5
vec2 mouse = (2.0 * uMouse - uResolution) / uResolution.y;
vec3 L = normalize(vec3(mouse, 1.5) - pos);
vec3 V = vec3(0.0, 0.0, 1.0);
float h = texture(uHeightMap, uv).r;
vec3 albedo = mix(vec3(0.3, 0.28, 0.25), vec3(0.5, 0.2, 0.12), smoothstep(0.05, 0.4, h));
// Blinn-Phong
float diffuse = max(dot(n, L), 0.0);
vec3 H = normalize(L + V);
float specular = pow(max(dot(n, H), 0.0), 32.0) * 0.25;
vec3 color = albedo * (0.05 + diffuse) + vec3(specular);
// 球の外は背景
if (r2 > 1.0) color = vec3(0.02, 0.025, 0.04);
color = pow(color, vec3(1.0 / 2.2));
out_color = vec4(color, 1.0);
}
cross(a, b)は線型代数第5章の外積です。
clamp(N.y, -1.0, 1.0)は、丸め誤差で−1〜1を少しだけはみ出した値からasinを守ります。
はみ出すとNaNを返してしまうからです。
texture()は球の外も含めて全てのピクセルで呼び、背景を選ぶのは其の後である事に注意して下さい。
第11章の一様な制御フローのルールです。
球の縁の近くではUVがピクセル毎にとても速く変わるので、ミップマップは其処で小さいレベルを選び、レンガが柔らかくなります。
此れは正しい動作です。
法線マップが何を足しているか見る為に、左半分では球自身の法線だけを使い(if (p.x < 0.0) n = N;)、光を上に置いた同じ球です:
左半分は色を塗ったボール、右半分はレンガで組んだ様に見えます。
どちらも輪郭は全く同じです。
法線マッピングが唯一変えられない物です。
光を動かすとこうなります:
12.8 良くある間違い
法線マッピングには典型的なバグが幾つかあります。
殆どは明らかに壊れているのではなく「何か少し変」なライティングになるので、見付けるのが難しいです。
緑チャンネル。
法線マップのには2つの流儀があります。
OpenGL流(Blender、Unity、glTFも)では緑は「に傾く」を意味し、今回のCのコードも其れを作ります。
DirectX流(Unreal、3ds Maxも)ではを反転して保存します。
DirectXの法線マップをOpenGL流のシェーダーで使うと、全ての凸凹が縦方向に逆から照らされている様に見えます。
次の画像の右半分はN.yを反転していて、光は上にあります:
左では上の斜面が光を受けています。
右では下の斜面が光を受けるので、レンガは下から照らされている様に見えるのに、横の斜面は正しいままで、混乱する見た目になります。
直すには、戻した後にn.y = -n.yとするか、画像の緑チャンネルを一度反転させます。
戻し忘れ。
色をそのまま法線として使う(* 2.0 - 1.0無し)と、法線がいつもに傾き、表面全体が右上から照らされている様に見えます。
正規化しない。
バイリニアフィルタリングとミップマップは隣の法線を平均し、単位ベクトルの平均は1より短くなります(反対向きの2つの斜面の平均は殆ど0です)。
戻した後に必ずnormalizeし、TBN行列を掛けた後にももう一度正規化して下さい。
歪んだTBN。
本物のメッシュではとは頂点から来て三角形の上で補間されるので、フラグメントシェーダーではもう長さ1でも完全に垂直でもありません。
を正規化し、から方向の成分を取り除き(内積による射影、線型代数第4章)、其れからを作ります:
此れをグラム・シュミットの直交化と呼び、殆ど全ての法線マッピングのフラグメントシェーダーの最初の行です。
sRGB。
法線マップと高さマップは色ではなくデータです。
GL_SRGB8_ALPHA8として読み込むと、サンプラーが元々無かったガンマを「戻して」しまい、全ての法線が曲がります。
色のテクスチャはsRGB、データのテクスチャは線型です(第11章の一覧)。
空間が混ざっている。
内積を取る前に、、、は同じ空間に無ければいけません。
接線空間の法線とワールド空間の光の方向の内積を取ると、物体が回るとライティングが変わってしまいます。
12.9 実際のゲームやエンジンでの使われ方
- ベイク:アーティストはとても細かいモデル(何百万もの三角形)を彫刻し、簡単なゲーム用のモデルを作り、ツール(Blender、Substance、Marmoset、xNormal)で其の差を法線マップにベイク(焼き込み)します。キャラクターの皺、縫い目、ボタンが形状無しで存在するのは此の為です。
- MikkTSpace:メッシュの接線は、ベイクするツールとエンジンで全く同じ方法で計算しなければならず、そうでないとベイクに継ぎ目や間違った陰が出ます。標準はMorten MikkelsenのMikkTSpaceで、Blender、Unity、Unreal、glTFが使っています。
- 頂点データの接線:メッシュは頂点毎に接線を
vec4で持ちます。xyzが、wは鏡映したUVの為の利き手(handedness)でかです。シェーダーはを計算します。そういうメッシュの読み込みはOpenGLシリーズで扱います。 - 2チャンネル圧縮:表面から外を向いた単位法線はなので、エンジンはとだけを保存し(PCではBC5、モバイルではEAC RG11)、をシェーダーで復元する事が良くあります。
- ディテール法線マップ:レンガの形の為の大きな法線マップと、細かい粒の為の何度も繰り返す小さな法線マップを組み合わせます。単純な平均ではなく、ブレンドの式(例えば「ホワイトアウト」:
normalize(vec3(a.xy + b.xy, a.z * b.z)))で混ぜます。 - 視差マッピング:法線マッピングはライティングを直しますが、横から見るとテクスチャはまだ平らに表面の上を滑ります。視差マッピングと視差遮蔽マッピングは高さマップを使って視線方向にUVをずらし、レンガが飛び出している様にも見せます。
- トライプラナーマッピング:地形や岩には良いUVが無い事が良くあります。トライプラナーマッピングは、、方向に投影した3回のサンプリングを法線で混ぜます。法線マップは投影毎に自分の小さなTBNが必要です。
- 事前計算した接線無しで:プロシージャルな表面では、フラグメントシェーダーが位置とUVの
dFdxとdFdyからTBNを作る事が出来ます。Christian Schülerが広めた手法で、12.5節の微分の技の一般版です。
12.10 まとめ
- ライティングは法線しか見ないので、法線のテクスチャで形状を増やさずに細部を足せます。輪郭を変えたり本物の影を落としたりは出来ません。
- 高さマップからはで法線が得られます。2つの傾きの方向の外積です。傾きは中心差分で測ります。
- 法線はで保存し、で読み戻します。平らならで、だから法線マップは青いです。
- 複数のテクスチャは複数のユニットを使います:
glActiveTexture(GL_TEXTURE1)、バインド、glUniform1i(…, 1)。アクティブなユニットはグローバルな状態なので、ユニット0のテクスチャを変える前に戻して下さい。 - 法線はシェーダーでも計算出来ます。中心差分(4回のサンプリング、滑らか)か、
dFdxとdFdy(タダ、ブロック状、簡単な形では画面と揃った面だけ)です。 - 法線マップは接線空間にあります。(
mat3(T, B, N))がライティングの空間に変換し、其の転置で戻ります。 - 球では:
atanとasinでUV、、。 - 緑チャンネルの流儀、戻し忘れ、正規化、補間された接線のグラム・シュミット、sRGB、空間の混在に注意。
12.11 課題
- ★ 或るテクセルの隣の高さが、左0.75、右0.25、下0.5、上0.5です。強さで(正規化した)法線と、保存されるRGBのバイト(Cのコードの様に255を掛けて0.5を足し、切り捨てる)を計算して下さい。どちらに傾いていますか?
- ★ 球の上の或る点でです。とを計算し、が「球の上向き」である事を確かめ、接線空間の法線をTBN行列で変換して下さい。
- ★★ 強さをアニメーションさせて下さい。正規化する前に、戻した法線のとを
0.5 + 0.5 * sin(uTime)倍して、凸凹が時間で大きくなったり小さくなったりする様にします。0の時は壁が完全に平らに、1の時は12.4節の法線マップの壁と同じに見える筈です。 - ★★ デバッグの為に法線を色で表示して下さい。球で、ライティングした色の代わりに
n * 0.5 + 0.5を出力します。球の右側はピンクっぽく、上は緑っぽく、レンガは小さな色の変化として見える筈です。其の後T * 0.5 + 0.5を出力し、見える色を説明して下さい。 - ★★ を復元して下さい。法線マップから
.rgだけを戻し、n.z = sqrt(max(1.0 - dot(n.xy, n.xy), 0.0))を計算します。壁は12.4節と全く同じに見えなければいけません。何故此れがBC5圧縮が2チャンネルしか保存しない理由なのでしょうか? - ★★ 第11章と第12章を組み合わせて下さい。法線マップを貼った球で、レンガの色の代わりに第11章のテスト用テクスチャ
uTexture(ユニット0)を同じUVでアルベドとして使います。チェッカーボードとFがレンガの上に描かれた様に見える筈です。其の後、法線マップの緑チャンネルを反転させ、どの斜面が変わるか説明して下さい。 - ★★★ 壁に簡単な視差マッピングを付けて下さい。サンプリングの前に、を高さ、
viewTangentを接線空間での目への方向として、uv += viewTangent.xy / viewTangent.z * h * 0.03でUVをずらします。今の真正面からの視点では何も起きないので、目の位置をマウスに依存させて下さい(例えばV = normalize(vec3(mouse, 1.0) - pos))。目が動くと、レンガが目地の上を滑る様に見える筈です。とても浅い角度では何故効果がおかしくなるのでしょうか?
ファイル: a/main.c | b/main.c
| 前 | 新 | ||
|---|---|---|---|
| @@ -288,6 +288,134 @@ int keyPressed(GLFWwindow *window, int key, int *wasDown) { | |||
| 288 | return pressed; | 288 | return pressed; |
| 289 | } | 289 | } |
| 290 | 290 | ||
| 291 | // ピクセルデータからテクスチャを作る(TEX_SIZE×TEX_SIZE、ミップマップ付き、GL_REPEAT) | ||
| 292 | GLuint uploadTexture(GLint internalFormat, GLenum format, const unsigned char *pixels) { | ||
| 293 | GLuint texture; | ||
| 294 | glGenTextures(1, &texture); | ||
| 295 | glBindTexture(GL_TEXTURE_2D, texture); | ||
| 296 | glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, TEX_SIZE, TEX_SIZE, 0, | ||
| 297 | format, GL_UNSIGNED_BYTE, pixels); | ||
| 298 | glGenerateMipmap(GL_TEXTURE_2D); | ||
| 299 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); | ||
| 300 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | ||
| 301 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); | ||
| 302 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); | ||
| 303 | return texture; | ||
| 304 | } | ||
| 305 | |||
| 306 | // GLSLのsmoothstepと同じ(0〜1の滑らかな段差) | ||
| 307 | float smoothstepf(float edge0, float edge1, float x) { | ||
| 308 | float t = (x - edge0) / (edge1 - edge0); | ||
| 309 | if (t < 0.0f) t = 0.0f; | ||
| 310 | if (t > 1.0f) t = 1.0f; | ||
| 311 | return t * t * (3.0f - 2.0f * t); | ||
| 312 | } | ||
| 313 | |||
| 314 | // 整数の座標から0〜1の疑似乱数を作るハッシュ(第8章と同じ考え方) | ||
| 315 | float hash2(int x, int y) { | ||
| 316 | unsigned int h = (unsigned int)x * 374761393u + (unsigned int)y * 668265263u; | ||
| 317 | h = (h ^ (h >> 13)) * 1274126177u; | ||
| 318 | return (float)((h ^ (h >> 16)) & 0xffffu) / 65535.0f; | ||
| 319 | } | ||
| 320 | |||
| 321 | // cellテクセル毎の値ノイズ(TEX_SIZEで繰り返すので、テクスチャの継ぎ目が見えない) | ||
| 322 | float valueNoise(int x, int y, int cell) { | ||
| 323 | int n = TEX_SIZE / cell; | ||
| 324 | int cx = x / cell, cy = y / cell; | ||
| 325 | float fx = smoothstepf(0.0f, 1.0f, (float)(x % cell) / cell); | ||
| 326 | float fy = smoothstepf(0.0f, 1.0f, (float)(y % cell) / cell); | ||
| 327 | float a = hash2(cx % n, cy % n); | ||
| 328 | float b = hash2((cx + 1) % n, cy % n); | ||
| 329 | float c = hash2(cx % n, (cy + 1) % n); | ||
| 330 | float d = hash2((cx + 1) % n, (cy + 1) % n); | ||
| 331 | return (a + (b - a) * fx) * (1.0f - fy) + (c + (d - c) * fx) * fy; | ||
| 332 | } | ||
| 333 | |||
| 334 | // レンガの高さマップ(0〜1)を作る:レンガは高く、目地は低く、レンガの縁は斜め | ||
| 335 | float *createBrickHeights(void) { | ||
| 336 | float *heights = (float *)malloc(TEX_SIZE * TEX_SIZE * sizeof(float)); | ||
| 337 | if (!heights) { | ||
| 338 | puts("高さマップのメモリ確保に失敗しました"); | ||
| 339 | return NULL; | ||
| 340 | } | ||
| 341 | |||
| 342 | for (int y = 0; y < TEX_SIZE; y++) { | ||
| 343 | for (int x = 0; x < TEX_SIZE; x++) { | ||
| 344 | // レンガは64×32テクセル、1段毎に半分ずらす | ||
| 345 | int row = y / 32; | ||
| 346 | int bx = (x + (row % 2) * 32) % 64; | ||
| 347 | int by = y % 32; | ||
| 348 | |||
| 349 | // レンガの一番近い縁までの距離(テクセル) | ||
| 350 | float ex = fminf(bx + 0.5f, 64.0f - (bx + 0.5f)); | ||
| 351 | float ey = fminf(by + 0.5f, 32.0f - (by + 0.5f)); | ||
| 352 | float e = fminf(ex, ey); | ||
| 353 | |||
| 354 | // 縁から2テクセルは目地(高さ0)、其処から4テクセルかけて斜めに上がる | ||
| 355 | float h = smoothstepf(2.0f, 6.0f, e); | ||
| 356 | |||
| 357 | // レンガの表面の細かい凸凹 | ||
| 358 | float rough = 0.6f * valueNoise(x, y, 8) + 0.4f * valueNoise(x, y, 4); | ||
| 359 | h *= 0.8f + 0.2f * rough; | ||
| 360 | |||
| 361 | heights[y * TEX_SIZE + x] = h; | ||
| 362 | } | ||
| 363 | } | ||
| 364 | return heights; | ||
| 365 | } | ||
| 366 | |||
| 367 | // 高さマップをそのまま1チャンネルのテクスチャにする(GL_R8) | ||
| 368 | GLuint createHeightTexture(const float *heights) { | ||
| 369 | if (!heights) return 0; | ||
| 370 | unsigned char *pixels = (unsigned char *)malloc(TEX_SIZE * TEX_SIZE); | ||
| 371 | if (!pixels) return 0; | ||
| 372 | for (int i = 0; i < TEX_SIZE * TEX_SIZE; i++) { | ||
| 373 | pixels[i] = (unsigned char)(heights[i] * 255.0f + 0.5f); | ||
| 374 | } | ||
| 375 | GLuint texture = uploadTexture(GL_R8, GL_RED, pixels); | ||
| 376 | free(pixels); | ||
| 377 | return texture; | ||
| 378 | } | ||
| 379 | |||
| 380 | // 高さマップから法線マップを作る(中心差分、OpenGLの「緑が上」の向き) | ||
| 381 | GLuint createNormalTexture(const float *heights, float strength) { | ||
| 382 | if (!heights) return 0; | ||
| 383 | unsigned char *pixels = (unsigned char *)malloc(TEX_SIZE * TEX_SIZE * 4); | ||
| 384 | if (!pixels) return 0; | ||
| 385 | |||
| 386 | for (int y = 0; y < TEX_SIZE; y++) { | ||
| 387 | for (int x = 0; x < TEX_SIZE; x++) { | ||
| 388 | // 隣のテクセル(端では反対側に回り込む、GL_REPEATと同じ) | ||
| 389 | int xl = (x + TEX_SIZE - 1) % TEX_SIZE, xr = (x + 1) % TEX_SIZE; | ||
| 390 | int yd = (y + TEX_SIZE - 1) % TEX_SIZE, yu = (y + 1) % TEX_SIZE; | ||
| 391 | |||
| 392 | // 1テクセル進んだ時の高さの変化 | ||
| 393 | float dhdx = (heights[y * TEX_SIZE + xr] - heights[y * TEX_SIZE + xl]) * 0.5f; | ||
| 394 | float dhdy = (heights[yu * TEX_SIZE + x] - heights[yd * TEX_SIZE + x]) * 0.5f; | ||
| 395 | |||
| 396 | // 法線 = (-s・dh/dx, -s・dh/dy, 1) を正規化 | ||
| 397 | float nx = -strength * dhdx; | ||
| 398 | float ny = -strength * dhdy; | ||
| 399 | float nz = 1.0f; | ||
| 400 | float len = sqrtf(nx * nx + ny * ny + nz * nz); | ||
| 401 | nx /= len; | ||
| 402 | ny /= len; | ||
| 403 | nz /= len; | ||
| 404 | |||
| 405 | // -1〜1を0〜255に詰める | ||
| 406 | unsigned char *p = &pixels[(y * TEX_SIZE + x) * 4]; | ||
| 407 | p[0] = (unsigned char)((nx * 0.5f + 0.5f) * 255.0f + 0.5f); | ||
| 408 | p[1] = (unsigned char)((ny * 0.5f + 0.5f) * 255.0f + 0.5f); | ||
| 409 | p[2] = (unsigned char)((nz * 0.5f + 0.5f) * 255.0f + 0.5f); | ||
| 410 | p[3] = 255; | ||
| 411 | } | ||
| 412 | } | ||
| 413 | |||
| 414 | GLuint texture = uploadTexture(GL_RGBA8, GL_RGBA, pixels); | ||
| 415 | free(pixels); | ||
| 416 | return texture; | ||
| 417 | } | ||
| 418 | |||
| 291 | int main(int argc, char **argv) { | 419 | int main(int argc, char **argv) { |
| 292 | (void)argc; | 420 | (void)argc; |
| 293 | (void)argv; | 421 | (void)argv; |
| @@ -380,6 +508,12 @@ int main(int argc, char **argv) { | |||
| 380 | setFilter(filterMode); | 508 | setFilter(filterMode); |
| 381 | setWrap(wrapMode); | 509 | setWrap(wrapMode); |
| 382 | 510 | ||
| 511 | // レンガの法線マップと高さマップを作る(第12章) | ||
| 512 | float *heights = createBrickHeights(); | ||
| 513 | GLuint normalMap = createNormalTexture(heights, 3.0f); | ||
| 514 | GLuint heightMap = createHeightTexture(heights); | ||
| 515 | free(heights); | ||
| 516 | |||
| 383 | int reloadWasDown = 0; | 517 | int reloadWasDown = 0; |
| 384 | int filterWasDown = 0; | 518 | int filterWasDown = 0; |
| 385 | int wrapWasDown = 0; | 519 | int wrapWasDown = 0; |
| @@ -390,6 +524,8 @@ int main(int argc, char **argv) { | |||
| 390 | if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); | 524 | if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); |
| 391 | 525 | ||
| 392 | // Fキーでフィルタリング、Wキーでラップモードを切り替える | 526 | // Fキーでフィルタリング、Wキーでラップモードを切り替える |
| 527 | // (アクティブなユニットは前のフレームの最後のままなので、ユニット0に戻す) | ||
| 528 | glActiveTexture(GL_TEXTURE0); | ||
| 393 | glBindTexture(GL_TEXTURE_2D, texture); | 529 | glBindTexture(GL_TEXTURE_2D, texture); |
| 394 | if (keyPressed(window, GLFW_KEY_F, &filterWasDown)) { | 530 | if (keyPressed(window, GLFW_KEY_F, &filterWasDown)) { |
| 395 | filterMode = (filterMode + 1) % 3; | 531 | filterMode = (filterMode + 1) % 3; |
| @@ -437,6 +573,14 @@ int main(int argc, char **argv) { | |||
| 437 | glBindTexture(GL_TEXTURE_2D, texture); | 573 | glBindTexture(GL_TEXTURE_2D, texture); |
| 438 | glUniform1i(glGetUniformLocation(shaderProgram, "uTexture"), 0); | 574 | glUniform1i(glGetUniformLocation(shaderProgram, "uTexture"), 0); |
| 439 | 575 | ||
| 576 | // 法線マップはユニット1、高さマップはユニット2 | ||
| 577 | glActiveTexture(GL_TEXTURE1); | ||
| 578 | glBindTexture(GL_TEXTURE_2D, normalMap); | ||
| 579 | glUniform1i(glGetUniformLocation(shaderProgram, "uNormalMap"), 1); | ||
| 580 | glActiveTexture(GL_TEXTURE2); | ||
| 581 | glBindTexture(GL_TEXTURE_2D, heightMap); | ||
| 582 | glUniform1i(glGetUniformLocation(shaderProgram, "uHeightMap"), 2); | ||
| 583 | |||
| 440 | // クアッドを描画 | 584 | // クアッドを描画 |
| 441 | glBindVertexArray(VAO); | 585 | glBindVertexArray(VAO); |
| 442 | glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); | 586 | glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); |
| @@ -451,6 +595,8 @@ int main(int argc, char **argv) { | |||
| 451 | glDeleteBuffers(1, &VBO); | 595 | glDeleteBuffers(1, &VBO); |
| 452 | glDeleteBuffers(1, &EBO); | 596 | glDeleteBuffers(1, &EBO); |
| 453 | glDeleteTextures(1, &texture); | 597 | glDeleteTextures(1, &texture); |
| 598 | glDeleteTextures(1, &normalMap); | ||
| 599 | glDeleteTextures(1, &heightMap); | ||
| 454 | glDeleteProgram(shaderProgram); | 600 | glDeleteProgram(shaderProgram); |
| 455 | glfwDestroyWindow(window); | 601 | glfwDestroyWindow(window); |
| 456 | glfwTerminate(); | 602 | glfwTerminate(); |
ファイル: a/shader/playground.glsl | b/shader/playground.glsl
| 前 | 新 | ||
|---|---|---|---|
| @@ -5,20 +5,54 @@ out vec4 out_color; | |||
| 5 | 5 | ||
| 6 | uniform float uTime; | 6 | uniform float uTime; |
| 7 | uniform vec2 uResolution; | 7 | uniform vec2 uResolution; |
| 8 | uniform sampler2D uTexture; | 8 | uniform vec2 uMouse; |
| 9 | uniform sampler2D uNormalMap; | ||
| 10 | uniform sampler2D uHeightMap; | ||
| 11 | |||
| 12 | const float PI = 3.14159265; | ||
| 9 | 13 | ||
| 10 | void main() { | 14 | void main() { |
| 11 | vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; | 15 | vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; |
| 12 | 16 | ||
| 13 | // 床の上の点:地平線(p.y = 0)に近い程遠い(距離は 1 / -p.y) | 17 | // 半径0.85の球(第9章の球と同じ考え方:zは x² + y² + z² = 1 から) |
| 14 | float depth = 1.0 / max(-p.y, 0.001); | 18 | vec2 q = p / 0.85; |
| 15 | vec2 uv = vec2(p.x * depth, depth) * 0.5; | 19 | float r2 = dot(q, q); |
| 16 | uv.y += uTime * 0.25; // 前に進む | 20 | vec3 N = vec3(q, sqrt(max(1.0 - r2, 0.0))); // 半径1の球では位置 = 法線 |
| 21 | vec3 pos = vec3(p, N.z * 0.85); | ||
| 22 | |||
| 23 | // 球面のUV:経度がu、緯度がv(uを時間でずらして自転させる) | ||
| 24 | vec2 uv; | ||
| 25 | uv.x = atan(N.x, N.z) / (2.0 * PI) + 0.5 + uTime * 0.03; | ||
| 26 | uv.y = asin(clamp(N.y, -1.0, 1.0)) / PI + 0.5; | ||
| 27 | uv *= vec2(4.0, 2.0); // 横に4回、縦に2回繰り返すとレンガが歪まない | ||
| 28 | |||
| 29 | // 接線空間の基底:Tはuが増える方向、Bはvが増える方向 | ||
| 30 | vec3 T = vec3(N.z, 0.0, -N.x); | ||
| 31 | T = length(T) > 1e-4 ? normalize(T) : vec3(1.0, 0.0, 0.0); // 極ではTが決まらない | ||
| 32 | vec3 B = cross(N, T); | ||
| 33 | mat3 TBN = mat3(T, B, N); // 列がT、B、N | ||
| 34 | |||
| 35 | // 法線マップの法線(接線空間)をビュー空間に変換する | ||
| 36 | vec3 nTangent = texture(uNormalMap, uv).rgb * 2.0 - 1.0; | ||
| 37 | vec3 n = normalize(TBN * nTangent); | ||
| 38 | |||
| 39 | // 光はマウスの位置、球の手前1.5 | ||
| 40 | vec2 mouse = (2.0 * uMouse - uResolution) / uResolution.y; | ||
| 41 | vec3 L = normalize(vec3(mouse, 1.5) - pos); | ||
| 42 | vec3 V = vec3(0.0, 0.0, 1.0); | ||
| 43 | |||
| 44 | float h = texture(uHeightMap, uv).r; | ||
| 45 | vec3 albedo = mix(vec3(0.3, 0.28, 0.25), vec3(0.5, 0.2, 0.12), smoothstep(0.05, 0.4, h)); | ||
| 46 | |||
| 47 | // Blinn-Phong | ||
| 48 | float diffuse = max(dot(n, L), 0.0); | ||
| 49 | vec3 H = normalize(L + V); | ||
| 50 | float specular = pow(max(dot(n, H), 0.0), 32.0) * 0.25; | ||
| 51 | vec3 color = albedo * (0.05 + diffuse) + vec3(specular); | ||
| 17 | 52 | ||
| 18 | // texture()はifの外で呼ぶ(理由は本文で) | 53 | // 球の外は背景 |
| 19 | vec3 floorColor = texture(uTexture, uv).rgb; | 54 | if (r2 > 1.0) color = vec3(0.02, 0.025, 0.04); |
| 20 | vec3 skyColor = vec3(0.45, 0.6, 0.85); | ||
| 21 | vec3 color = p.y < 0.0 ? floorColor : skyColor; | ||
| 22 | 55 | ||
| 56 | color = pow(color, vec3(1.0 / 2.2)); | ||
| 23 | out_color = vec4(color, 1.0); | 57 | out_color = vec4(color, 1.0); |
| 24 | } | 58 | } |
コメント(0)
まだコメントはありません。
ログインするとコメント出来ます。