diff --git a/main.c b/main.c index 56a1fe2..56a6b31 100755 --- a/main.c +++ b/main.c @@ -133,50 +133,85 @@ GLuint compileShader(GLenum type, const char *source) { return shader; } -// 頂点シェーダーとフラグメントシェーダーのファイルからプログラムを作る(失敗したら0) -GLuint buildProgram(const char *vertexPath, const char *fragmentPath) { - char *vertexSource = readShaderFile(vertexPath); - char *fragmentSource = readShaderFile(fragmentPath); - if (!vertexSource || !fragmentSource) { - free(vertexSource); - free(fragmentSource); - return 0; - } +// シェーダーファイルを読み込んでコンパイルする(失敗したら0) +GLuint compileShaderFile(GLenum type, const char *path) { + char *source = readShaderFile(path); + if (!source) return 0; #ifdef __APPLE__ // macOSはOpenGL 4.1までなので、「#version 450」を「#version 410」に書き換える char *v; - if ((v = strstr(vertexSource, "#version 450"))) v[10] = '1'; - if ((v = strstr(fragmentSource, "#version 450"))) v[10] = '1'; + if ((v = strstr(source, "#version 450"))) v[10] = '1'; #endif - GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexSource); - GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentSource); - free(vertexSource); - free(fragmentSource); - if (!vertexShader || !fragmentShader) { - if (vertexShader) glDeleteShader(vertexShader); - if (fragmentShader) glDeleteShader(fragmentShader); - return 0; + GLuint shader = compileShader(type, source); + free(source); + return shader; +} + +// シェーダーステージの種類(パスの配列の順番) +enum { STAGE_VERTEX, STAGE_TESS_CONTROL, STAGE_TESS_EVALUATION, STAGE_GEOMETRY, STAGE_FRAGMENT, + STAGE_COMPUTE, STAGE_COUNT }; + +static const GLenum gStageTypes[STAGE_COUNT] = { + GL_VERTEX_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER, + GL_FRAGMENT_SHADER, +#ifdef __APPLE__ + 0, // macOS(OpenGL 4.1)にはコンピュートシェーダーがない +#else + GL_COMPUTE_SHADER, +#endif +}; + +// 好きなステージの組み合わせからプログラムを作る(NULLのステージは使わない、失敗したら0) +GLuint buildProgramStages(const char *const paths[STAGE_COUNT]) { + GLuint shaders[STAGE_COUNT] = { 0 }; + int ok = 1; + for (int i = 0; i < STAGE_COUNT; i++) { + if (!paths[i]) continue; + if (!gStageTypes[i]) { + printf("此のプラットフォームでは使えないシェーダーです: %s\n", paths[i]); + ok = 0; + break; + } + shaders[i] = compileShaderFile(gStageTypes[i], paths[i]); + if (!shaders[i]) { + ok = 0; + break; + } } - GLuint program = glCreateProgram(); - glAttachShader(program, vertexShader); - glAttachShader(program, fragmentShader); - glLinkProgram(program); - glDeleteShader(vertexShader); - glDeleteShader(fragmentShader); + GLuint program = 0; + if (ok) { + program = glCreateProgram(); + for (int i = 0; i < STAGE_COUNT; i++) { + if (shaders[i]) glAttachShader(program, shaders[i]); + } + glLinkProgram(program); + + GLint success; + glGetProgramiv(program, GL_LINK_STATUS, &success); + if (!success) { + checkProgramLink(program); + glDeleteProgram(program); + program = 0; + } + } - GLint success; - glGetProgramiv(program, GL_LINK_STATUS, &success); - if (!success) { - checkProgramLink(program); - glDeleteProgram(program); - return 0; + for (int i = 0; i < STAGE_COUNT; i++) { + if (shaders[i]) glDeleteShader(shaders[i]); } return program; } +// 頂点シェーダーとフラグメントシェーダーのファイルからプログラムを作る(失敗したら0) +GLuint buildProgram(const char *vertexPath, const char *fragmentPath) { + const char *paths[STAGE_COUNT] = { NULL }; + paths[STAGE_VERTEX] = vertexPath; + paths[STAGE_FRAGMENT] = fragmentPath; + return buildProgramStages(paths); +} + #define PI 3.14159265358979f // メッシュ(頂点配列オブジェクトとインデックスの数) @@ -332,6 +367,40 @@ Mesh createGrid(int nx, int ny, float w, float h) { return mesh; } +// XZ平面上のパッチの格子(一辺size、中心が原点):n × n個のパッチ、1パッチ=4頂点 +Mesh createPatchGrid(int n, float size) { + int vertexCount = (n + 1) * (n + 1); + int indexCount = n * n * 4; + float *vertices = malloc(vertexCount * VERTEX_FLOATS * sizeof(float)); + GLuint *indices = malloc(indexCount * sizeof(GLuint)); + + float *out = vertices; + for (int j = 0; j <= n; j++) { + for (int i = 0; i <= n; i++) { + float u = (float)i / (float)n, v = (float)j / (float)n; + out[0] = (u - 0.5f) * size; out[1] = 0.0f; out[2] = (v - 0.5f) * size; + out[3] = 0.0f; out[4] = 1.0f; out[5] = 0.0f; + out[6] = u; out[7] = v; + out += VERTEX_FLOATS; + } + } + + // 1パッチの頂点の順番:(u, v) = (0, 0)、(1, 0)、(1, 1)、(0, 1) + GLuint *idx = indices; + for (int j = 0; j < n; j++) { + for (int i = 0; i < n; i++) { + GLuint a = j * (n + 1) + i; + GLuint b = a + n + 1; + *idx++ = a; *idx++ = a + 1; *idx++ = b + 1; *idx++ = b; + } + } + + Mesh mesh = createMesh(vertices, vertexCount, indices, indexCount); + free(vertices); + free(indices); + return mesh; +} + // 行列のユニフォームを設定(列優先のままなのでtransposeはGL_FALSE) void setMat4(GLuint program, const char *name, Matrix4 m) { glUniformMatrix4fv(glGetUniformLocation(program, name), 1, GL_FALSE, m.m); @@ -343,25 +412,42 @@ void setMat3(GLuint program, const char *name, Matrix3 m) { // シェーダープログラムの一覧(Rキーで全部再読み込み) typedef struct { - const char *vertexPath; - const char *fragmentPath; + const char *paths[STAGE_COUNT]; // 頂点、テッセレーション制御、テッセレーション評価、ジオメトリ、フラグメント、コンピュート GLuint id; } Program; -enum { PROGRAM_SKY, PROGRAM_PHONG, PROGRAM_GOURAUD, PROGRAM_FLAG, PROGRAM_COUNT }; +enum { + PROGRAM_SKY, PROGRAM_PHONG, PROGRAM_GOURAUD, PROGRAM_FLAG, + PROGRAM_NORMALS, PROGRAM_EXPLODE, PROGRAM_TERRAIN, PROGRAM_LIFE_STEP, PROGRAM_LIFE_SHOW, + PROGRAM_COUNT +}; static Program gPrograms[PROGRAM_COUNT] = { - { "shader/vertex_fullscreen.glsl", "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 }, + { { "shader/vertex_fullscreen.glsl", NULL, NULL, NULL, "shader/frag_sky.glsl", NULL }, 0 }, + { { "shader/vertex_mesh.glsl", NULL, NULL, NULL, "shader/frag_phong.glsl", NULL }, 0 }, + { { "shader/vertex_gouraud.glsl", NULL, NULL, NULL, "shader/frag_gouraud.glsl", NULL }, 0 }, + { { "shader/vertex_flag.glsl", NULL, NULL, NULL, "shader/frag_phong.glsl", NULL }, 0 }, + // ジオメトリシェーダー:法線を線で描く、爆発 + { { "shader/vertex_passthrough.glsl", NULL, NULL, "shader/geom_normals.glsl", "shader/frag_line.glsl", + NULL }, 0 }, + { { "shader/vertex_passthrough.glsl", NULL, NULL, "shader/geom_explode.glsl", "shader/frag_phong.glsl", + NULL }, 0 }, + // テッセレーション:地形 + { { "shader/vertex_terrain.glsl", "shader/tesc_terrain.glsl", "shader/tese_terrain.glsl", NULL, + "shader/frag_terrain.glsl", NULL }, 0 }, + // コンピュートシェーダー:ライフゲーム(macOSでは使えない) + { { NULL, NULL, NULL, NULL, NULL, "shader/comp_life.glsl" }, 0 }, + { { "shader/vertex_fullscreen.glsl", NULL, NULL, NULL, "shader/frag_life.glsl", NULL }, 0 }, }; // 全てのプログラムを作り直す(失敗した物は前のままにする)、失敗の数を返す int loadPrograms(void) { int failed = 0; for (int i = 0; i < PROGRAM_COUNT; i++) { - GLuint program = buildProgram(gPrograms[i].vertexPath, gPrograms[i].fragmentPath); +#ifdef __APPLE__ + if (gPrograms[i].paths[STAGE_COMPUTE] || i == PROGRAM_LIFE_SHOW) continue; // デモ4はmacOSでは無し +#endif + GLuint program = buildProgramStages(gPrograms[i].paths); if (program) { if (gPrograms[i].id) glDeleteProgram(gPrograms[i].id); gPrograms[i].id = program; @@ -391,6 +477,191 @@ void setCommonUniforms(GLuint program, Matrix4 view, Matrix4 proj, Vector3 eye, glUniform1f(glGetUniformLocation(program, "uTime"), time); } +// 全てのデモで使うメッシュ +static Mesh gCube, gSphere, gSmoothSphere, gFlag, gTerrain; +static GLuint gEmptyVAO; + +// 背景:頂点バッファなしのフルスクリーン三角形(深度テストなし) +void drawSky(void) { + glDisable(GL_DEPTH_TEST); + GLuint sky = gPrograms[PROGRAM_SKY].id; + glUseProgram(sky); + glUniform2f(glGetUniformLocation(sky, "uResolution"), (float)gWidth, (float)gHeight); + glBindVertexArray(gEmptyVAO); + glDrawArrays(GL_TRIANGLES, 0, 3); + glEnable(GL_DEPTH_TEST); +} + +Matrix4 cameraProjection(void) { + float aspect = gHeight > 0 ? (float)gWidth / (float)gHeight : 1.0f; + return mat4Perspective(50.0f * PI / 180.0f, aspect, 0.1f, 100.0f); +} + +// デモ1:第13章のシーン +void drawSceneDemo(float time, int useGouraud) { + drawSky(); + + // カメラ:ビュー行列と投影行列 + 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); + Matrix4 proj = cameraProjection(); + + // 立方体と球(Gキーでシェーダーを切り替え) + GLuint lit = useGouraud ? gPrograms[PROGRAM_GOURAUD].id : gPrograms[PROGRAM_PHONG].id; + setCommonUniforms(lit, view, proj, eye, 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, &gCube, 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, &gSphere, 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, &gCube, poleModel, poleColor, 0); + + // 旗:頂点シェーダーで波打たせる(両面を描くのでカリングなし) + 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, &gFlag, flagModel, flagColor, 2); + glEnable(GL_CULL_FACE); +} + +// デモ2:ジオメトリシェーダー(左:爆発する球、右:法線を線で描いた球) +void drawGeometryDemo(float time) { + drawSky(); + + 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); + Matrix4 proj = cameraProjection(); + + // 爆発:三角形を面の法線の方向へ押し出す(裏側も見えるのでカリングなし) + GLuint explode = gPrograms[PROGRAM_EXPLODE].id; + setCommonUniforms(explode, view, proj, eye, time); + float amount = 0.5f * (0.5f - 0.5f * cosf(time * 1.2f)); // 0〜0.5を行ったり来たり + glUniform1f(glGetUniformLocation(explode, "uExplode"), amount); + Matrix4 leftModel = mat4Multiply(mat4Translate(-1.3f, 0.1f, 0.0f), + mat4Multiply(mat4RotateY(time * 0.4f), mat4Scale(0.8f, 0.8f, 0.8f))); + Vector3 leftColor = { { 0.9f, 0.3f, 0.35f } }; + glDisable(GL_CULL_FACE); + drawLit(explode, &gSmoothSphere, leftModel, leftColor, 1); + glEnable(GL_CULL_FACE); + + // 右:普通に描いてから、同じメッシュの法線を線で重ねる + Matrix4 rightModel = mat4Multiply(mat4Translate(1.3f, 0.1f, 0.0f), + mat4Multiply(mat4RotateY(time * 0.4f), mat4Scale(0.9f, 0.6f, 0.9f))); + GLuint lit = gPrograms[PROGRAM_PHONG].id; + setCommonUniforms(lit, view, proj, eye, time); + Vector3 rightColor = { { 0.2f, 0.45f, 0.9f } }; + drawLit(lit, &gSphere, rightModel, rightColor, 0); + + GLuint normals = gPrograms[PROGRAM_NORMALS].id; + setCommonUniforms(normals, view, proj, eye, time); + glUniform1f(glGetUniformLocation(normals, "uLength"), 0.2f); + drawLit(normals, &gSphere, rightModel, rightColor, 0); +} + +// デモ3:テッセレーションで地形を細かくする +void drawTessellationDemo(float time, int wireframe) { + drawSky(); + + // カメラ:地形の周りをゆっくり回る + float angle = time * 0.15f; + Vector3 eye = { { 8.0f * sinf(angle), 6.0f, 8.0f * cosf(angle) } }; + Vector3 target = { { 0.0f, 0.0f, 0.0f } }; + Vector3 up = { { 0.0f, 1.0f, 0.0f } }; + Matrix4 view = mat4LookAt(eye, target, up); + Matrix4 proj = cameraProjection(); + + GLuint terrain = gPrograms[PROGRAM_TERRAIN].id; + setCommonUniforms(terrain, view, proj, eye, time); + + if (wireframe) glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + glDisable(GL_CULL_FACE); + glPatchParameteri(GL_PATCH_VERTICES, 4); // 1パッチ=4頂点 + glBindVertexArray(gTerrain.vao); + glDrawElements(GL_PATCHES, gTerrain.indexCount, GL_UNSIGNED_INT, 0); + glEnable(GL_CULL_FACE); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +} + +#ifndef __APPLE__ +// デモ4:コンピュートシェーダーのライフゲーム(OpenGL 4.3以上) +#define LIFE_WIDTH 200 +#define LIFE_HEIGHT 150 + +static GLuint gLifeTextures[2]; +static int gLifeCurrent = 0; + +// セルをランダムに置く(r=生きているか、g=残像) +void resetLife(unsigned int seed) { + static unsigned char cells[LIFE_WIDTH * LIFE_HEIGHT * 4]; + for (int i = 0; i < LIFE_WIDTH * LIFE_HEIGHT; i++) { + seed = seed * 1664525u + 1013904223u; // 線型合同法 + unsigned char alive = (seed >> 24) < 64 ? 255 : 0; // 約25% + cells[i * 4 + 0] = alive; + cells[i * 4 + 1] = alive; + cells[i * 4 + 2] = 0; + cells[i * 4 + 3] = 255; + } + glBindTexture(GL_TEXTURE_2D, gLifeTextures[gLifeCurrent]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, LIFE_WIDTH, LIFE_HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, cells); +} + +void createLife(void) { + glGenTextures(2, gLifeTextures); + for (int i = 0; i < 2; i++) { + glBindTexture(GL_TEXTURE_2D, gLifeTextures[i]); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, LIFE_WIDTH, LIFE_HEIGHT); // 大きさと形式を固定 + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + resetLife(76); +} + +// 1世代進める:今のテクスチャを読み、もう一方に書く +void stepLife(void) { + GLuint program = gPrograms[PROGRAM_LIFE_STEP].id; + glUseProgram(program); + glBindImageTexture(0, gLifeTextures[gLifeCurrent], 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8); + glBindImageTexture(1, gLifeTextures[1 - gLifeCurrent], 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); + + // ワークグループは16 × 16なので、切り上げて画像全体を覆う + glDispatchCompute((LIFE_WIDTH + 15) / 16, (LIFE_HEIGHT + 15) / 16, 1); + + // 書き込みが終わってから次の読み込み(imageLoadとtexture)をする + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_TEXTURE_FETCH_BARRIER_BIT); + gLifeCurrent = 1 - gLifeCurrent; +} + +void drawComputeDemo(void) { + glDisable(GL_DEPTH_TEST); + GLuint show = gPrograms[PROGRAM_LIFE_SHOW].id; + glUseProgram(show); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, gLifeTextures[gLifeCurrent]); + glUniform1i(glGetUniformLocation(show, "uLife"), 0); + glBindVertexArray(gEmptyVAO); + glDrawArrays(GL_TRIANGLES, 0, 3); + glEnable(GL_DEPTH_TEST); +} +#endif + int main(int argc, char **argv) { (void)argc; (void)argv; @@ -417,7 +688,7 @@ int main(int argc, char **argv) { #endif // ウィンドウを作成 - GLFWwindow *window = glfwCreateWindow(800, 600, "GLSLシェーダー:頂点シェーダーと3D", NULL, NULL); + GLFWwindow *window = glfwCreateWindow(800, 600, "1:3Dシーン", NULL, NULL); if (!window) { puts("GLFWウィンドウの作成に失敗しました"); glfwTerminate(); @@ -447,13 +718,18 @@ int main(int argc, char **argv) { } // メッシュを作る - Mesh cube = createCube(); - Mesh sphere = createSphere(16, 12); - Mesh flag = createGrid(32, 20, 1.4f, 0.9f); + gCube = createCube(); + gSphere = createSphere(16, 12); + gSmoothSphere = createSphere(32, 24); + gFlag = createGrid(32, 20, 1.4f, 0.9f); + gTerrain = createPatchGrid(8, 10.0f); // 頂点バッファなしで描く為の空のVAO(コアプロファイルではVAOが必須) - GLuint emptyVAO; - glGenVertexArrays(1, &emptyVAO); + glGenVertexArrays(1, &gEmptyVAO); + +#ifndef __APPLE__ + createLife(); +#endif // 深度テストと裏面カリング glEnable(GL_DEPTH_TEST); @@ -461,15 +737,31 @@ int main(int argc, char **argv) { glCullFace(GL_BACK); glFrontFace(GL_CCW); - int reloadWasDown = 0; - int gouraudWasDown = 0; - int useGouraud = 0; + const char *demoNames[4] = { + "1:3Dシーン", "2:ジオメトリシェーダー", "3:テッセレーション", "4:コンピュートシェーダー" + }; + int demo = 0; + int reloadWasDown = 0, gouraudWasDown = 0, wireWasDown = 0, spaceWasDown = 0; + int useGouraud = 0, wireframe = 0; + float lastLifeStep = 0.0f; // メインレンダリングループ while (!glfwWindowShouldClose(window)) { // ESCキーで終了 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, 1); + // 1〜4キーでデモを切り替える + for (int i = 0; i < 4; i++) { + if (glfwGetKey(window, GLFW_KEY_1 + i) == GLFW_PRESS && demo != i) { +#ifdef __APPLE__ + if (i == 3) continue; // コンピュートシェーダーはmacOSでは使えない +#endif + demo = i; + glfwSetWindowTitle(window, demoNames[i]); + puts(demoNames[i]); + } + } + // Rキーでシェーダーを再読み込み(失敗したら古いシェーダーのまま) int reloadDown = glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS; if (reloadDown && !reloadWasDown) { @@ -486,59 +778,33 @@ int main(int argc, char **argv) { } gouraudWasDown = gouraudDown; + // Wキーでワイヤーフレーム(デモ3) + int wireDown = glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS; + if (wireDown && !wireWasDown) wireframe = !wireframe; + wireWasDown = wireDown; + float time = (float)glfwGetTime(); // 画面と深度バッファをクリア glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - // 背景:頂点バッファなしのフルスクリーン三角形(深度テストなし) - 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); - - // カメラ:ビュー行列と投影行列 - 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); - - // 立方体と球(Gキーでシェーダーを切り替え) - GLuint lit = useGouraud ? gPrograms[PROGRAM_GOURAUD].id : gPrograms[PROGRAM_PHONG].id; - setCommonUniforms(lit, view, proj, eye, 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); - - // 旗:頂点シェーダーで波打たせる(両面を描くのでカリングなし) - 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); + if (demo == 0) drawSceneDemo(time, useGouraud); + else if (demo == 1) drawGeometryDemo(time); + else if (demo == 2) drawTessellationDemo(time, wireframe); +#ifndef __APPLE__ + else { + // スペースキーでやり直し、1秒に20世代進める + int spaceDown = glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS; + if (spaceDown && !spaceWasDown) resetLife((unsigned int)(time * 1000.0f)); + spaceWasDown = spaceDown; + if (time - lastLifeStep >= 0.05f) { + stepLife(); + lastLifeStep = time; + } + drawComputeDemo(); + } +#endif // バッファをスワップし、イベントを処理 glfwSwapBuffers(window); @@ -546,10 +812,15 @@ int main(int argc, char **argv) { } // クリーンアップ - destroyMesh(&cube); - destroyMesh(&sphere); - destroyMesh(&flag); - glDeleteVertexArrays(1, &emptyVAO); + destroyMesh(&gCube); + destroyMesh(&gSphere); + destroyMesh(&gSmoothSphere); + destroyMesh(&gFlag); + destroyMesh(&gTerrain); + glDeleteVertexArrays(1, &gEmptyVAO); +#ifndef __APPLE__ + glDeleteTextures(2, gLifeTextures); +#endif for (int i = 0; i < PROGRAM_COUNT; i++) glDeleteProgram(gPrograms[i].id); glfwDestroyWindow(window); glfwTerminate(); diff --git a/shader/comp_life.glsl b/shader/comp_life.glsl new file mode 100644 index 0000000..be8efc8 --- /dev/null +++ b/shader/comp_life.glsl @@ -0,0 +1,32 @@ +#version 450 core + +// 1つのワークグループ=16 × 16個の呼び出し +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +layout(rgba8, binding = 0) uniform readonly image2D uCurrent; // 今の世代 +layout(rgba8, binding = 1) uniform writeonly image2D uNext; // 次の世代 + +void main() { + ivec2 size = imageSize(uCurrent); + ivec2 p = ivec2(gl_GlobalInvocationID.xy); // 此の呼び出しが担当するセル + if (p.x >= size.x || p.y >= size.y) return; // 端のワークグループのはみ出し + + // 周りの8つのセルを数える(端は反対側に繋がる) + int neighbours = 0; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; + ivec2 q = (p + ivec2(dx, dy) + size) % size; + if (imageLoad(uCurrent, q).r > 0.5) neighbours++; + } + } + + // ライフゲームの規則:生きているセルは2か3で生き残り、死んでいるセルは3で生まれる + vec4 cell = imageLoad(uCurrent, p); + bool alive = cell.r > 0.5; + bool next = alive ? (neighbours == 2 || neighbours == 3) : (neighbours == 3); + + // g:残像(死んだ後ゆっくり消える) + float trail = next ? 1.0 : cell.g * 0.92; + imageStore(uNext, p, vec4(next ? 1.0 : 0.0, trail, 0.0, 1.0)); +} diff --git a/shader/frag_life.glsl b/shader/frag_life.glsl new file mode 100644 index 0000000..df520e6 --- /dev/null +++ b/shader/frag_life.glsl @@ -0,0 +1,13 @@ +#version 450 core + +in vec2 vUv; +out vec4 out_color; + +uniform sampler2D uLife; // コンピュートシェーダーが書いたテクスチャ + +void main() { + vec4 cell = texture(uLife, vUv); + vec3 trail = vec3(0.15, 0.1, 0.45) * cell.g; // 残像:紫 + vec3 alive = vec3(1.0, 0.9, 0.5) * cell.r; // 生きているセル:明るい黄色 + out_color = vec4(max(trail, alive), 1.0); +} diff --git a/shader/frag_line.glsl b/shader/frag_line.glsl new file mode 100644 index 0000000..3add7c2 --- /dev/null +++ b/shader/frag_line.glsl @@ -0,0 +1,8 @@ +#version 450 core + +in vec3 gColor; +out vec4 out_color; + +void main() { + out_color = vec4(gColor, 1.0); +} diff --git a/shader/frag_terrain.glsl b/shader/frag_terrain.glsl new file mode 100644 index 0000000..8ec0553 --- /dev/null +++ b/shader/frag_terrain.glsl @@ -0,0 +1,32 @@ +#version 450 core + +in vec3 vWorldPos; +in vec3 vNormal; +out vec4 out_color; + +uniform vec3 uLightDir; +uniform vec3 uCameraPos; + +void main() { + vec3 n = normalize(vNormal); + float h = vWorldPos.y; + + // 高さと傾きで色を決める + vec3 rock = vec3(0.3, 0.27, 0.25); + vec3 color = vec3(0.05, 0.15, 0.35); // 水 + color = mix(color, vec3(0.6, 0.55, 0.35), smoothstep(0.0, 0.05, h)); // 砂 + color = mix(color, vec3(0.12, 0.35, 0.08), smoothstep(0.1, 0.3, h)); // 草 + color = mix(color, rock, smoothstep(0.55, 0.8, h)); // 岩 + color = mix(color, rock, 1.0 - smoothstep(0.6, 0.75, n.y)); // 急な斜面も岩 + color = mix(color, vec3(0.9), smoothstep(1.1, 1.3, h)); // 雪 + + float diffuse = max(dot(n, normalize(uLightDir)), 0.0); + vec3 lit = color * (0.15 + diffuse); + + // 遠い程空の色に + float d = distance(vWorldPos, uCameraPos); + float fog = 1.0 - exp(-0.002 * d * d); + lit = mix(lit, vec3(0.32, 0.30, 0.36), fog); + + out_color = vec4(pow(lit, vec3(1.0 / 2.2)), 1.0); +} diff --git a/shader/geom_explode.glsl b/shader/geom_explode.glsl new file mode 100644 index 0000000..7ce58b3 --- /dev/null +++ b/shader/geom_explode.glsl @@ -0,0 +1,38 @@ +#version 450 core + +layout(triangles) in; +layout(triangle_strip, max_vertices = 3) out; + +in vec3 vsPosition[]; +in vec3 vsNormal[]; +in vec2 vsUv[]; + +// フラグメントシェーダー(frag_phong.glsl)が受け取る名前 +out vec3 vWorldPos; +out vec3 vNormal; +out vec2 vUv; + +uniform mat4 uModel; +uniform mat4 uView; +uniform mat4 uProj; +uniform mat3 uNormalMatrix; +uniform float uExplode; // 押し出す距離 + +void main() { + vec3 world[3]; + for (int i = 0; i < 3; i++) world[i] = (uModel * vec4(vsPosition[i], 1.0)).xyz; + + // 面の法線:二つの辺の外積(線型代数の第5章) + vec3 faceNormal = normalize(cross(world[1] - world[0], world[2] - world[0])); + vec3 offset = faceNormal * uExplode; + + mat4 viewProj = uProj * uView; + for (int i = 0; i < 3; i++) { + vWorldPos = world[i] + offset; + vNormal = uNormalMatrix * vsNormal[i]; + vUv = vsUv[i]; + gl_Position = viewProj * vec4(vWorldPos, 1.0); + EmitVertex(); + } + EndPrimitive(); +} diff --git a/shader/geom_normals.glsl b/shader/geom_normals.glsl new file mode 100644 index 0000000..63f8a64 --- /dev/null +++ b/shader/geom_normals.glsl @@ -0,0 +1,33 @@ +#version 450 core + +layout(triangles) in; // 入力:三角形(頂点3つ) +layout(line_strip, max_vertices = 6) out; // 出力:線(頂点2つ × 3本) + +in vec3 vsPosition[]; // 頂点シェーダーの出力は配列になる +in vec3 vsNormal[]; + +out vec3 gColor; + +uniform mat4 uModel; +uniform mat4 uView; +uniform mat4 uProj; +uniform mat3 uNormalMatrix; +uniform float uLength; // 線の長さ(ワールド空間) + +void main() { + mat4 viewProj = uProj * uView; + for (int i = 0; i < 3; i++) { + vec3 p = (uModel * vec4(vsPosition[i], 1.0)).xyz; + vec3 n = normalize(uNormalMatrix * vsNormal[i]); + + gColor = vec3(1.0, 0.9, 0.2); // 根元:黄色 + gl_Position = viewProj * vec4(p, 1.0); + EmitVertex(); + + gColor = vec3(1.0, 0.2, 0.1); // 先:赤 + gl_Position = viewProj * vec4(p + n * uLength, 1.0); + EmitVertex(); + + EndPrimitive(); // 1本の線が終わり + } +} diff --git a/shader/tesc_terrain.glsl b/shader/tesc_terrain.glsl new file mode 100644 index 0000000..add3cdc --- /dev/null +++ b/shader/tesc_terrain.glsl @@ -0,0 +1,30 @@ +#version 450 core + +layout(vertices = 4) out; // 出力パッチの制御点の数 + +in vec3 vsPosition[]; +out vec3 tcPosition[]; + +uniform vec3 uCameraPos; + +// 辺の中点がカメラに近い程細かくする +float edgeLevel(vec3 a, vec3 b) { + float d = distance(0.5 * (a + b), uCameraPos); + return clamp(96.0 / d, 1.0, 64.0); +} + +void main() { + // 制御点はそのまま渡す(gl_InvocationIDは0〜3) + tcPosition[gl_InvocationID] = vsPosition[gl_InvocationID]; + + // 分割数はパッチに1回だけ決めれば良い + if (gl_InvocationID == 0) { + // 角の順番:0 = (0, 0)、1 = (1, 0)、2 = (1, 1)、3 = (0, 1) + gl_TessLevelOuter[0] = edgeLevel(vsPosition[3], vsPosition[0]); // u = 0の辺 + gl_TessLevelOuter[1] = edgeLevel(vsPosition[0], vsPosition[1]); // v = 0の辺 + gl_TessLevelOuter[2] = edgeLevel(vsPosition[1], vsPosition[2]); // u = 1の辺 + gl_TessLevelOuter[3] = edgeLevel(vsPosition[2], vsPosition[3]); // v = 1の辺 + gl_TessLevelInner[0] = max(gl_TessLevelOuter[1], gl_TessLevelOuter[3]); // u方向 + gl_TessLevelInner[1] = max(gl_TessLevelOuter[0], gl_TessLevelOuter[2]); // v方向 + } +} diff --git a/shader/tese_terrain.glsl b/shader/tese_terrain.glsl new file mode 100644 index 0000000..5d74d4b --- /dev/null +++ b/shader/tese_terrain.glsl @@ -0,0 +1,65 @@ +#version 450 core + +layout(quads, fractional_odd_spacing, ccw) in; + +in vec3 tcPosition[]; + +out vec3 vWorldPos; +out vec3 vNormal; + +uniform mat4 uView; +uniform mat4 uProj; + +// 第8章の様なバリューノイズとfBm +float hash(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float valueNoise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +float fbm(vec2 p) { + float sum = 0.0; + float amplitude = 0.5; + for (int i = 0; i < 4; i++) { + sum += amplitude * valueNoise(p); + p *= 2.0; + amplitude *= 0.5; + } + return sum; +} + +// 地形の高さ(0より下は水面で平らにする) +float terrainHeight(vec2 xz) { + return max(3.0 * fbm(xz * 0.3) - 1.1, 0.0); +} + +void main() { + // gl_TessCoord:パッチの中の位置(u, v)、0〜1 + vec2 uv = gl_TessCoord.xy; + vec3 bottom = mix(tcPosition[0], tcPosition[1], uv.x); + vec3 top = mix(tcPosition[3], tcPosition[2], uv.x); + vec3 p = mix(bottom, top, uv.y); + + // 高さで押し上げる(ディスプレイスメント) + p.y = terrainHeight(p.xz); + + // 法線:高さの差分から(y = h(x, z) の勾配) + const float e = 0.02; + float hx = terrainHeight(p.xz + vec2(e, 0.0)) - terrainHeight(p.xz - vec2(e, 0.0)); + float hz = terrainHeight(p.xz + vec2(0.0, e)) - terrainHeight(p.xz - vec2(0.0, e)); + vNormal = normalize(vec3(-hx, 2.0 * e, -hz)); + + vWorldPos = p; + gl_Position = uProj * uView * vec4(p, 1.0); +} diff --git a/shader/vertex_passthrough.glsl b/shader/vertex_passthrough.glsl new file mode 100644 index 0000000..2f311e5 --- /dev/null +++ b/shader/vertex_passthrough.glsl @@ -0,0 +1,16 @@ +#version 450 core + +layout(location = 0) in vec3 aPosition; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aUv; + +// 何も変換せず、そのままジオメトリシェーダーへ渡す +out vec3 vsPosition; +out vec3 vsNormal; +out vec2 vsUv; + +void main() { + vsPosition = aPosition; + vsNormal = aNormal; + vsUv = aUv; +} diff --git a/shader/vertex_terrain.glsl b/shader/vertex_terrain.glsl new file mode 100644 index 0000000..03fa2d0 --- /dev/null +++ b/shader/vertex_terrain.glsl @@ -0,0 +1,9 @@ +#version 450 core + +layout(location = 0) in vec3 aPosition; + +out vec3 vsPosition; // パッチの角(ワールド空間) + +void main() { + vsPosition = aPosition; +}