目錄
- 已實現功能
- 陰影
- shadowMap
- PCF
- PCSS
- 實現
- shadowMap
- PCF
- PCSS
- 陰影
GitHub主頁:https://github.com/sdpyy1
OpenGLRender:https://github.com/sdpyy1/CppLearn/tree/main/OpenGL
已實現功能
除了上次實現IBL之外,項目目前新增了imGUI的渲染,更方便地進行調試
可以隨意切換IBL貼圖、模型控制、燈光控制燈。 并且添加了一個PBR材質的地板。下一步就是實現陰影
陰影
shadowMap
這個東西很簡單,直接實現了,不講原理
PCF
取周圍一圈的像素求平均,讓陰影更軟
PCSS
① Blocker Search 階段(尋找遮擋者)
目的:估算遮擋物與被遮擋物之間的距離 → 用于計算 penumbra(陰影模糊程度)
步驟:
從當前 fragment 的 light space 坐標,投影到 shadow map 中:projCoords.xy
以該位置為中心,在 shadow map 中進行 小范圍采樣(通常 3x3 或 5x5):
收集所有 比當前 fragment 深度更小的樣本(說明它們擋住了光)
累加這些“遮擋者”的深度值
記錄 blocker 數量
若存在 blocker:
計算平均 blocker 深度 avgBlockerDepth
② Penumbra Size 計算階段(決定模糊程度)
目的:用當前 fragment 深度 與 avgBlockerDepth 的距離估算光源發散導致的陰影模糊程度
③ Filtering 階段(模糊陰影邊緣)
目的:根據 penumbra 大小,用可變范圍 PCF 模糊陰影邊緣
所以PCSS可以叫自適應PCF
實現
shadowMap
初始化一個FBO用于shadowMap的渲染,創建一張紋理存儲深度結果
void ShadowPass::init() {glGenFramebuffers(1, &shadowFBO);// 創建深度紋理glGenTextures(1, &shadowMap);glBindTexture(GL_TEXTURE_2D, shadowMap);glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,scene.width, scene.height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);// attach 深度紋理到FBOglBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowMap, 0);glDrawBuffer(GL_NONE);glReadBuffer(GL_NONE);glBindFramebuffer(GL_FRAMEBUFFER, 0);isInit = true;
}
下一步就是在光源視角下渲染,首先得找到攝像機的位置,其實就是MVP矩陣的VP用光源而不是用攝像機,下面是對于平行光的shadowMap渲染
void ShadowPass::render() {if (!isInit){std::cout << "shadowPass init" << std::endl;return;}glViewport(0, 0, scene.width, scene.height);glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);glClear(GL_DEPTH_BUFFER_BIT);// glCullFace(GL_FRONT); // 可選,防止 Peter-panningshadowShader.bind();glm::mat4 lightProjection, lightView;float orthoSize = 10.0f;float near_plane = 0.1f;float far_plane = 100.0f; // 你可以再根據場景大小動態調整// 平行光使用正交投影lightProjection = glm::ortho(-orthoSize, orthoSize, -orthoSize, orthoSize, near_plane, far_plane);// TODO:只實現了平行光,他的position存儲的是方向,而不是位置,所以要取反lightView = glm::lookAt(-scene.lights[0]->position * 10.0f, glm::vec3(0.0f), glm::vec3(0.0, 1.0, 0.0));lightSpaceMatrix = lightProjection * lightView;shadowShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);// 渲染所有模型(只寫深度)for (auto& model : scene.models) {glm::mat4 modelMatrix = model.getModelMatrix();shadowShader.setMat4("model", modelMatrix);model.draw(shadowShader);}shadowShader.unBind();
// glCullFace(GL_BACK);glBindFramebuffer(GL_FRAMEBUFFER, 0);glViewport(0, 0, scene.width, scene.height);
}
頂點著色器,物體要乘以lightSpaceMatrix來轉到光源視角下
#version 330 core
layout (location = 0) in vec3 aPos;uniform mat4 model;
uniform mat4 lightSpaceMatrix;void main() {gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}
片段著色器不需要執行操作
#version 330 core
void main() {// 空著就行,只寫深度
}
下一步將生成的shadowMap傳遞到lightPass來參與光照計算,另外需要把lightMatrix也傳遞過去,用于把模型距離值轉移到視角下來進行比較
float ShadowCalculation(vec3 fragPosWorld, vec3 normal) {vec4 fragPosLightSpace = lightSpaceMatrix * vec4(fragPosWorld, 1.0);vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;projCoords = projCoords * 0.5 + 0.5; // [-1,1] → [0,1]// 從深度貼圖采樣float closestDepth = texture(shadowMap, projCoords.xy).r;float currentDepth = projCoords.z;// 簡單 bias 防止 shadow acne
// float bias = max(0.005 * (1.0 - dot(normal, normalize(lightPos))), 0.001);// 超出邊界不產生陰影if (projCoords.z > 1.0)return 0.0;// 進行一次比較return (currentDepth) > closestDepth ? 1.0 : 0.0;
}
有了計算陰影的函數后,只需要在計算光照的最后*(1-shadow)
vec3 Lo = (kD * albedo / PI + specular) * radiance * NdotL * (1-ShadowCalculation(WorldPos, N));
結果如下,經典的自陰影現象,主要原因就是shadowMap的分辨率不足,一些不在同一高度的位置被記錄了相同高度,在主攝像機渲染時,某個位置在shadowMap存儲的高度比自己本來還要高,就會被認為是陰影(但是這種情況下很好分辨光源攝像機的覆蓋范圍,方便調試🙂)
當我把shadowMap的分辨率提高后,自然就消失了,但這肯定不是最優
通常的做法是加一個自偏移,也就是說shadowMap存的高度和我用來比較的高度差異不超過bias,就認為沒有陰影
float ShadowCalculation(vec3 fragPosWorld, vec3 normal) {vec4 fragPosLightSpace = lightSpaceMatrix * vec4(fragPosWorld, 1.0);vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;projCoords = projCoords * 0.5 + 0.5; // [-1,1] → [0,1]// 從深度貼圖采樣float closestDepth = texture(shadowMap, projCoords.xy).r;float currentDepth = projCoords.z;// 簡單 bias 防止 shadow acnefloat bias = max(0.005 * (1.0 - dot(normal, normalize(lightPos))), 0.001);// 超出邊界不產生陰影if (projCoords.z > 1.0)return 0.0;// 進行一次比較return (currentDepth - bias) > closestDepth ? 1.0 : 0.0;
}
PCF
陰影問題解決了,下面就是提升效果,當前的陰影是硬陰影,鋸齒很嚴重
PCF思路就是取周圍像素的shadow來取平均,柔化陰影邊界
// --- PCF ---float shadow = 0.0;ivec2 texSize = textureSize(shadowMap, 0);vec2 texelSize = 1.0 /vec2(texSize);int range = 10; // 5x5int samples = 0;for (int x = -range; x <= range; ++x) {for (int y = -range; y <= range; ++y) {float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += (currentDepth - bias > pcfDepth) ? 1.0 : 0.0;samples++;}}shadow /= float(samples);return shadow;
PCSS
三步走,一些參數我已經提取出去當uniform,可以控制第一步的搜索半徑、第二步的半影大小、以及控制一下最大的濾波核大小
float avgBlockerDepth = 0.0;int blockers = 0;ivec2 texSize = textureSize(shadowMap, 0);vec2 texelSize = 1.0 / vec2(texSize);int searchRadius = int(PCSSBlockerSearchRadius);for (int x = -searchRadius; x <= searchRadius; ++x) {for (int y = -searchRadius; y <= searchRadius; ++y) {float sampleDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;if (sampleDepth < currentDepth - bias) {avgBlockerDepth += sampleDepth;blockers++;}}}if (blockers == 0) return 0.0;avgBlockerDepth /= blockers;float penumbra = (currentDepth - avgBlockerDepth) * PCSSScale;int kernel = int(clamp(penumbra * float(texSize.x), 1.0, PCSSKernelMax));float shadow = 0.0;for (int x = -kernel; x <= kernel; ++x) {for (int y = -kernel; y <= kernel; ++y) {float sampleDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += (currentDepth - bias > sampleDepth) ? 1.0 : 0.0;}}shadow /= float((2 * kernel + 1) * (2 * kernel + 1));return shadow;
陰影
三種陰影可以寫在一起
float ShadowCalculation(vec3 fragPosWorld, vec3 normal) {vec4 fragPosLightSpace = lightSpaceMatrix * vec4(fragPosWorld, 1.0);vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;projCoords = projCoords * 0.5 + 0.5;if (projCoords.z > 1.0) return 0.0;float closestDepth = texture(shadowMap, projCoords.xy).r;float currentDepth = projCoords.z;float bias = max(0.005 * (1.0 - dot(normal, normalize(lightPos))), 0.001);// shadow type switchingif (shadowType == 0) {return 0.0; // no shadow}else if (shadowType == 1) {return (currentDepth - bias > closestDepth) ? 1.0 : 0.0; // hard shadow}else if (shadowType == 2) {// --- PCF ---float shadow = 0.0;ivec2 texSize = textureSize(shadowMap, 0);vec2 texelSize = 1.0 /vec2(texSize);int range = pcfScope;int samples = 0;for (int x = -range; x <= range; ++x) {for (int y = -range; y <= range; ++y) {float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += (currentDepth - bias > pcfDepth) ? 1.0 : 0.0;samples++;}}shadow /= float(samples);return shadow;}else if (shadowType == 3) {float avgBlockerDepth = 0.0;int blockers = 0;ivec2 texSize = textureSize(shadowMap, 0);vec2 texelSize = 1.0 / vec2(texSize);int searchRadius = int(PCSSBlockerSearchRadius);for (int x = -searchRadius; x <= searchRadius; ++x) {for (int y = -searchRadius; y <= searchRadius; ++y) {float sampleDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;if (sampleDepth < currentDepth - bias) {avgBlockerDepth += sampleDepth;blockers++;}}}if (blockers == 0) return 0.0;avgBlockerDepth /= blockers;float penumbra = (currentDepth - avgBlockerDepth) * PCSSScale;int kernel = int(clamp(penumbra * float(texSize.x), 1.0, PCSSKernelMax));float shadow = 0.0;for (int x = -kernel; x <= kernel; ++x) {for (int y = -kernel; y <= kernel; ++y) {float sampleDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;shadow += (currentDepth - bias > sampleDepth) ? 1.0 : 0.0;}}shadow /= float((2 * kernel + 1) * (2 * kernel + 1));return shadow;}return 0.0;
}