From: Luke Lau Date: Mon, 19 Nov 2018 23:00:47 +0000 (+0000) Subject: Add specular component X-Git-Tag: cs7gv3-a3~16 X-Git-Url: https://git.lukelau.me/?p=opengl.git;a=commitdiff_plain;h=d80972d96e5fcd444657f937ab2700039efa83d2 Add specular component Also add hi-res GLUT display --- diff --git a/Makefile b/Makefile index 9ee7707..208ae4f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ all: main main: - clang++ -g --std=c++17 *.cpp -L/usr/local/lib -lassimp -framework OpenGL -framework glut -framework CoreGraphics -framework CoreFoundation -framework ImageIO -lglew -o bin/main + clang++ -g --std=c++17 *.cpp *.mm -L/usr/local/lib -lassimp -framework OpenGL -framework glut -framework CoreGraphics -framework CoreFoundation -framework Cocoa -framework ImageIO -lglew -o bin/main ctags *.cpp diff --git a/brdffrag.glsl b/brdffrag.glsl new file mode 100644 index 0000000..93403de --- /dev/null +++ b/brdffrag.glsl @@ -0,0 +1,90 @@ +#version 330 + +in vec2 texCoords; +out vec4 fragColor; + +const float PI = 3.14159265359; + +//TODO: Put this in a separate shader program +float radicalInverse(uint bits) { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; +} + +vec2 hammersley(uint i, uint N) { + return vec2(float(i) / float(N), radicalInverse(i)); +} + +float geometrySchlickGGX(float NdotV, float roughness) { + float a = roughness * roughness; + float k = (a * a) / 2.f; + + return NdotV / (NdotV * (1.f - k) + k); +} + +float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { + float ggx1 = geometrySchlickGGX(max(dot(N, L), 0.f), roughness); + float ggx2 = geometrySchlickGGX(max(dot(N, V), 0.f), roughness); + return ggx1 * ggx2; +} + +vec3 importanceSampleGGX(vec2 Xi, vec3 N, float roughness) { + float a = roughness * roughness; + + float phi = 2.f * PI * Xi.x; + float cosTheta = sqrt((1.f - Xi.y) / (1.f + (a * a - 1.f) * Xi.y)); + float sinTheta = sqrt(1.f - cosTheta * cosTheta); + + // spherical -> cartesian + vec3 H = vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); + + vec3 up = abs(N.z) < 0.999 ? vec3(0, 0, 1) : vec3(1, 0, 0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); + + vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; + return normalize(sampleVec); +} + +vec2 integrateBRDF(float NdotV, float roughness) { + vec3 V = vec3(sqrt(1.f - NdotV * NdotV), 0.f, NdotV); + + float A = 0, B = 0; + + vec3 N = vec3(0, 0, 1); + + const uint sampleCount = 1024u; + for (uint i = 0u; i < sampleCount; i++) { + vec2 Xi = hammersley(i, sampleCount); + vec3 H = importanceSampleGGX(Xi, N, roughness); + vec3 L = normalize(2.f * dot(V, H) * H - V); + + float NdotL = max(L.z, 0.f); + float NdotH = max(H.z, 0.f); + float VdotH = max(dot(V, H), 0.f); + + if (NdotL > 0) { + float G = geometrySmith(N, V, L, roughness); + float Gvis = (G * VdotH) / (NdotH * NdotV); + float Fc = pow(1.f - VdotH, 5.f); + + A += (1.f - Fc) * Gvis; + B += Fc * Gvis; + } + } + + A /= float(sampleCount); + B /= float(sampleCount); + + return vec2(A, B); +} + +void main() { + fragColor = vec4(integrateBRDF(texCoords.x, texCoords.y), 0, 0); +} + + diff --git a/brdfvert.glsl b/brdfvert.glsl new file mode 100644 index 0000000..11d63ef --- /dev/null +++ b/brdfvert.glsl @@ -0,0 +1,9 @@ +#version 330 + +in vec2 pos; +out vec2 texCoords; + +void main() { + texCoords = (pos + 1) / 2; + gl_Position = vec4(pos, 0, 1); +} diff --git a/cocoa.h b/cocoa.h new file mode 100644 index 0000000..5dcf66d --- /dev/null +++ b/cocoa.h @@ -0,0 +1 @@ +void swizzle(); diff --git a/cocoa.mm b/cocoa.mm new file mode 100644 index 0000000..ce2549b --- /dev/null +++ b/cocoa.mm @@ -0,0 +1,6 @@ +#import +void swizzle() { + NSWindow *window = [NSApp orderedWindows][0]; + NSView *view = [window contentView]; + [view setWantsBestResolutionOpenGLSurface: true]; +} diff --git a/main.cpp b/main.cpp index 8fbff62..9464e9e 100644 --- a/main.cpp +++ b/main.cpp @@ -5,6 +5,7 @@ #include #ifdef __APPLE__ #include +#include "cocoa.h" #else #include #endif @@ -169,6 +170,14 @@ void display() { glActiveTexture(GL_TEXTURE5); glBindTexture(GL_TEXTURE_CUBE_MAP, skybox->getIrradianceMap()); + glUniform1i(glGetUniformLocation(pbrProg->progId, "prefilterMap"), 6); + glActiveTexture(GL_TEXTURE6); + glBindTexture(GL_TEXTURE_CUBE_MAP, skybox->getPrefilterMap()); + + glUniform1i(glGetUniformLocation(pbrProg->progId, "brdfMap"), 7); + glActiveTexture(GL_TEXTURE7); + glBindTexture(GL_TEXTURE_2D, skybox->getBRDFMap()); + pbr->draw(); for (Light &light: lights) drawLight(light); @@ -220,14 +229,16 @@ void init() { glUseProgram(pbrProg->progId); pbr = new Model("models/sphere.dae", *pbrProg, *skybox); - albedoMap = loadTexture("models/darktiles/darktiles1_basecolor.png"); - metallicMap = loadTexture("models/darktiles/darktiles1_metallic.png"); - normalMap = loadTexture("models/darktiles/darktiles1_normal.png"); - roughnessMap = loadTexture("models/darktiles/darktiles1_roughness.png"); - aoMap = loadTexture("models/darktiles/darktiles1_AO.png"); + albedoMap = loadTexture("models/streakedmetal/streakedmetal_albedo.png"); + metallicMap = loadTexture("models/streakedmetal/streakedmetal_metalness.png"); + normalMap = loadTexture("models/streakedmetal/streakedmetal_normal.png"); + roughnessMap = loadTexture("models/streakedmetal/streakedmetal_roughness.png"); + aoMap = loadTexture("models/streakedmetal/streakedmetal_ao.png"); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); + // prevent edge artifacts in specular cubemaps + glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); } bool* keyStates = new bool[256]; @@ -322,6 +333,8 @@ int main(int argc, char** argv) { init(); + swizzle(); + glutKeyboardFunc(keyboard); glutKeyboardUpFunc(keyboardUp); glutTimerFunc(16, timer, 0); diff --git a/pbrfrag.glsl b/pbrfrag.glsl index 16a9fa7..99523ec 100644 --- a/pbrfrag.glsl +++ b/pbrfrag.glsl @@ -13,6 +13,8 @@ uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; uniform samplerCube irradianceMap; +uniform samplerCube prefilterMap; +uniform sampler2D brdfMap; out vec4 fragColor; @@ -76,6 +78,8 @@ void main() { vec3 F0 = mix(vec3(0.04), albedo, metallic); + vec3 R = reflect(-V, N); + // reflectance vec3 Lo = vec3(0.f); for (int i = 0; i < lightPositions.length(); i++) { @@ -102,9 +106,20 @@ void main() { Lo += (kD * albedo / PI + specular) * radiance * mdot(N, L); } - vec3 kD = 1.f - fresnelSchlickRoughness(mdot(N, V), F0, roughness); - vec3 diffuse = texture(irradianceMap, N).rgb * albedo; - vec3 ambient = (kD * diffuse) * ao; + vec3 F = fresnelSchlickRoughness(mdot(N, V), F0, roughness); + + vec3 kS = F; + vec3 kD = (1.f - kS) * (1.f - metallic); + + vec3 irradiance = texture(irradianceMap, N).rgb; + vec3 diffuse = irradiance * albedo; + + const float maxReflectionLoD = 4.f; + vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * maxReflectionLoD).rgb; + vec2 envBRDF = texture(brdfMap, vec2(mdot(N, V), roughness)).rg; + vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); + + vec3 ambient = (kD * diffuse + specular) * ao; vec3 color = ambient + Lo; color = color / (color + vec3(1.f)); // map to HDR diff --git a/prefilterfrag.glsl b/prefilterfrag.glsl new file mode 100644 index 0000000..87cd70e --- /dev/null +++ b/prefilterfrag.glsl @@ -0,0 +1,67 @@ +#version 330 + +in vec3 localPos, normal; +out vec4 fragColor; + +uniform samplerCube environmentMap; +uniform float roughness; + +const float PI = 3.14159265359; + +//TODO: Put this in a separate shader program +float radicalInverse(uint bits) { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; +} + +vec2 hammersley(uint i, uint N) { + return vec2(float(i) / float(N), radicalInverse(i)); +} + +vec3 importanceSampleGGX(vec2 Xi, vec3 N, float roughness) { + float a = roughness * roughness; + + float phi = 2.f * PI * Xi.x; + float cosTheta = sqrt((1.f - Xi.y) / (1.f + (a * a - 1.f) * Xi.y)); + float sinTheta = sqrt(1.f - cosTheta * cosTheta); + + // spherical -> cartesian + vec3 H = vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); + + vec3 up = abs(N.z) < 0.999 ? vec3(0, 0, 1) : vec3(1, 0, 0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); + + vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; + return normalize(sampleVec); +} + +void main() { + vec3 N = normalize(localPos); + + // approximate view direction - no grazing specular reflections + vec3 R = N; + vec3 V = R; + + const uint sampleCount = 1024u; + float totalWeight = 0; + vec3 prefilteredColor = vec3(0); + for (uint i = 0u; i < sampleCount; i++) { + vec2 Xi = hammersley(i, sampleCount); + vec3 H = importanceSampleGGX(Xi, N, roughness); + vec3 L = normalize(2.f * dot(V, H) * H - V); + + float NdotL = max(dot(N, L), 0.f); + if (NdotL > 0) { + prefilteredColor += texture(environmentMap, L).rgb * NdotL; + totalWeight += NdotL; + } + } + + prefilteredColor = prefilteredColor / totalWeight; + fragColor = vec4(prefilteredColor, 1.f); +} diff --git a/shapes.hpp b/shapes.hpp index 5fcefdb..63263b5 100644 --- a/shapes.hpp +++ b/shapes.hpp @@ -15,6 +15,15 @@ constexpr array quadToTriangles(const array quads) { }; } +constexpr array plane() { + return quadToTriangles({ + glm::vec3(1, 1, 0), + glm::vec3(-1, 1, 0), + glm::vec3(-1, -1, 0), + glm::vec3(1, -1, 0) + }); +} + constexpr array cube() { int i = 0; array vertices; diff --git a/skybox.cpp b/skybox.cpp index ab492ca..cb79fb3 100644 --- a/skybox.cpp +++ b/skybox.cpp @@ -2,15 +2,96 @@ #include "skybox.hpp" #include -GLuint setupCubeVertices(GLuint progId, bool reverse = false); +template +GLuint setupVertices(GLuint progId, std::array vertices, bool reverse = false); + +// matrices used when capturing various environment maps +glm::mat4 captureProj = glm::perspective(glm::radians(90.f), 1.f, 0.1f, 10.f); +glm::mat4 captureViews[] = { + glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0, 0, 0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 0, 1, 0), glm::vec3(0, 0, 1)), + glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 0, -1, 0), glm::vec3(0, 0, -1)), + glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 0, 0, 1), glm::vec3(0, -1, 0)), + glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 0, 0, -1), glm::vec3(0, -1, 0)) +}; + +void Skybox::generatePrefilterMap() const { + glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterTexId); + for (GLuint i = 0; i < 6; i++) + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0 , GL_RGB16F, 128, 128, 0, GL_RGB, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + + Program prefilterProg("skyboxvert.glsl", "prefilterfrag.glsl"); + glUseProgram(prefilterProg.progId); + glUniform1i(glGetUniformLocation(prefilterProg.progId, "environmentMap"), 0); + glUniformMatrix4fv(glGetUniformLocation(prefilterProg.progId, "projection"), 1, GL_FALSE, glm::value_ptr(captureProj)); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexId); + + setupVertices(prefilterProg.progId, cube()); + + glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); + constexpr GLuint MAX_MIP_LEVELS = 5; + for (GLuint mip = 0; mip < MAX_MIP_LEVELS; mip++) { + GLuint mipWidth = 128 * std::pow(0.5, mip); + GLuint mipHeight = 128 * std::pow(0.5, mip); + glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight); + glViewport(0, 0, mipWidth, mipHeight); + + float roughness = (float)mip / (MAX_MIP_LEVELS - 1.f); + glUniform1f(glGetUniformLocation(prefilterProg.progId, "roughness"), roughness); + + for (GLuint i = 0; i < 6; i++) { + glUniformMatrix4fv(glGetUniformLocation(prefilterProg.progId, "view"), 1, GL_FALSE, glm::value_ptr(captureViews[i])); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, prefilterTexId, mip); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glDrawArrays(GL_TRIANGLES, 0, 36); + } + } +} + +void Skybox::generateBRDFMap() const { + glBindTexture(GL_TEXTURE_2D, brdfMapTexId); + // allocate memory + glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); + glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brdfMapTexId, 0); + + glViewport(0, 0, 512, 512); + Program prog("brdfvert.glsl", "brdffrag.glsl"); + glUseProgram(prog.progId); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + setupVertices(prog.progId, plane()); + glDrawArrays(GL_TRIANGLES, 0, 6); +} Skybox::Skybox(const Image img): program("skyboxvert.glsl", "skyboxfrag.glsl") { Program equiProg("skyboxvert.glsl", "equirectangularfrag.glsl"); glUseProgram(equiProg.progId); - GLuint allTexIds[3]; - glGenTextures(3, allTexIds); - hdrTexId = allTexIds[0], cubemapTexId = allTexIds[1], irradianceTexId = allTexIds[2]; + GLuint allTexIds[5]; + glGenTextures(5, allTexIds); + hdrTexId = allTexIds[0]; + cubemapTexId = allTexIds[1]; + irradianceTexId = allTexIds[2]; + prefilterTexId = allTexIds[3]; + brdfMapTexId = allTexIds[4]; glBindTexture(GL_TEXTURE_2D, hdrTexId); @@ -22,7 +103,6 @@ Skybox::Skybox(const Image img): program("skyboxvert.glsl", "skyboxfrag.glsl") { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // generate framebuffers to store cubemap in - GLuint captureFBO, captureRBO; glGenFramebuffers(1, &captureFBO); glGenRenderbuffers(1, &captureRBO); @@ -42,18 +122,9 @@ Skybox::Skybox(const Image img): program("skyboxvert.glsl", "skyboxfrag.glsl") { glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // generate vertices - setupCubeVertices(equiProg.progId); + setupVertices(equiProg.progId, cube()); // render the cube - glm::mat4 captureProj = glm::perspective(glm::radians(90.f), 1.f, 0.1f, 10.f); - glm::mat4 captureViews[] = { - glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 1, 0, 0), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0, 0, 0), glm::vec3(-1, 0, 0), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 0, 1, 0), glm::vec3(0, 0, 1)), - glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 0, -1, 0), glm::vec3(0, 0, -1)), - glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 0, 0, 1), glm::vec3(0, -1, 0)), - glm::lookAt(glm::vec3(0, 0, 0), glm::vec3( 0, 0, -1), glm::vec3(0, -1, 0)) - }; glUniform1i(glGetUniformLocation(equiProg.progId, "equirectangularMap"), 0); glUniformMatrix4fv(glGetUniformLocation(equiProg.progId, "projection"), 1, GL_FALSE, glm::value_ptr(captureProj)); @@ -92,7 +163,7 @@ Skybox::Skybox(const Image img): program("skyboxvert.glsl", "skyboxfrag.glsl") { glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexId); // generate vertices - setupCubeVertices(irradianceProg.progId); + setupVertices(irradianceProg.progId, cube()); // render irradiance map glViewport(0, 0, 32, 32); @@ -104,12 +175,16 @@ Skybox::Skybox(const Image img): program("skyboxvert.glsl", "skyboxfrag.glsl") { glDrawArrays(GL_TRIANGLES, 0, 36); } + generatePrefilterMap(); + + generateBRDFMap(); + // switch back to regular skybox shader glUseProgram(program.progId); glDepthFunc(GL_LEQUAL); // reverse so facing inside out - vao = setupCubeVertices(program.progId, true); + vao = setupVertices(program.progId, cube(), true); // restore default framebuffer glBindFramebuffer(GL_FRAMEBUFFER, 0); @@ -128,13 +203,14 @@ void Skybox::draw(glm::mat4 proj, glm::mat4 view) const { glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceTexId); + glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexId); glDrawArrays(GL_TRIANGLES, 0, 36); if (glGetError()) exit(1); } -GLuint setupCubeVertices(GLuint progId, bool reverse) { +template +GLuint setupVertices(GLuint progId, std::array vertices, bool reverse) { GLuint vao; glGenVertexArrays(1, &vao); glBindVertexArray(vao); @@ -142,8 +218,6 @@ GLuint setupCubeVertices(GLuint progId, bool reverse) { GLuint vbo; glGenBuffers(1, &vbo); - auto vertices = cube(); - if (reverse) std::reverse(vertices.begin(), vertices.end()); diff --git a/skybox.hpp b/skybox.hpp index 40ed1a1..6961e8d 100644 --- a/skybox.hpp +++ b/skybox.hpp @@ -14,10 +14,15 @@ class Skybox { void draw(glm::mat4 proj, glm::mat4 view) const; GLuint getTexture() const { return cubemapTexId; } GLuint getIrradianceMap() const { return irradianceTexId; } + GLuint getPrefilterMap() const { return prefilterTexId; } + GLuint getBRDFMap() const { return brdfMapTexId; } private: - GLuint hdrTexId, cubemapTexId, irradianceTexId; + GLuint hdrTexId, cubemapTexId, irradianceTexId, prefilterTexId, brdfMapTexId; + GLuint captureFBO, captureRBO; GLuint vao; const Program program; + void generatePrefilterMap() const; + void generateBRDFMap() const; }; #endif