diff --git a/main.c b/main.c index 5a97397..cb6b1bf 100755 --- a/main.c +++ b/main.c @@ -288,6 +288,134 @@ int keyPressed(GLFWwindow *window, int key, int *wasDown) { return pressed; } +// ピクセルデータからテクスチャを作る(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; +} + +// 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; +} + +// 高さマップをそのまま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; +} + int main(int argc, char **argv) { (void)argc; (void)argv; @@ -380,6 +508,12 @@ int main(int argc, char **argv) { setFilter(filterMode); setWrap(wrapMode); + // レンガの法線マップと高さマップを作る(第12章) + float *heights = createBrickHeights(); + GLuint normalMap = createNormalTexture(heights, 3.0f); + GLuint heightMap = createHeightTexture(heights); + free(heights); + int reloadWasDown = 0; int filterWasDown = 0; int wrapWasDown = 0; @@ -390,6 +524,8 @@ int main(int argc, char **argv) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); // Fキーでフィルタリング、Wキーでラップモードを切り替える + // (アクティブなユニットは前のフレームの最後のままなので、ユニット0に戻す) + glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); if (keyPressed(window, GLFW_KEY_F, &filterWasDown)) { filterMode = (filterMode + 1) % 3; @@ -437,6 +573,14 @@ int main(int argc, char **argv) { glBindTexture(GL_TEXTURE_2D, texture); glUniform1i(glGetUniformLocation(shaderProgram, "uTexture"), 0); + // 法線マップはユニット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); + // クアッドを描画 glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); @@ -451,6 +595,8 @@ int main(int argc, char **argv) { glDeleteBuffers(1, &VBO); glDeleteBuffers(1, &EBO); glDeleteTextures(1, &texture); + glDeleteTextures(1, &normalMap); + glDeleteTextures(1, &heightMap); glDeleteProgram(shaderProgram); glfwDestroyWindow(window); glfwTerminate(); diff --git a/shader/playground.glsl b/shader/playground.glsl index a9dd86b..92925ae 100644 --- a/shader/playground.glsl +++ b/shader/playground.glsl @@ -5,20 +5,54 @@ out vec4 out_color; uniform float uTime; uniform vec2 uResolution; -uniform sampler2D uTexture; +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; - // 床の上の点:地平線(p.y = 0)に近い程遠い(距離は 1 / -p.y) - float depth = 1.0 / max(-p.y, 0.001); - vec2 uv = vec2(p.x * depth, depth) * 0.5; - uv.y += uTime * 0.25; // 前に進む + // 半径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); - // texture()はifの外で呼ぶ(理由は本文で) - vec3 floorColor = texture(uTexture, uv).rgb; - vec3 skyColor = vec3(0.45, 0.6, 0.85); - vec3 color = p.y < 0.0 ? floorColor : skyColor; + // 球の外は背景 + 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); }