学习OpenGL ES之ShadowMap(二)
获取示例代码
本文将为大家解决上一篇文章留下来的问题。
Shadow acne
在上一篇文末,我们看到了有问题的阴影效果,我们常把这种问题称为Shadow acne,在不该出现阴影的地方出现了亮暗相间的条纹。我们通过下面这张图解释产生问题的原因。
黄色部分代表从深度纹理获取到的深度值,因为纹理是有分辨率限制的,不同位置的相邻像素点会采样到同一个深度值。比如三个深度值分别为0.6,0.7,0.8的像素点,没有像素点遮挡它们,但是他们采样到的深度值可能都是0.7,那么深度为0.8的像素点就会变暗。我们可以通过增加采样到的深度值的方法避免这个问题。我们把增加的值称为
shadow bias
。对应的Fragment Shader代码如下。我们将采样到的深度值shadowColor.r
增加0.005
再做比较。
if (shadowUV.x >= 0.0 && shadowUV.x <=1.0 && shadowUV.y >= 0.0 && shadowUV.y <=1.0) {
vec4 shadowColor = texture2D(shadowMap, shadowUV);
if (shadowColor.r + 0.005 < positionInLightSpace.z) {
shadow = 0.1;
} else {
shadow = 1.0;
}
}
修复后效果如下。
因为面的法线不同,需要调整的深度值也不一样,一般会把bias设置成与法线相关的一个值。
float bias = 0.005*tan(acos(dot(transformedNormal, normalizedLightDirection)));
bias = clamp(bias, 0.0, 0.01);
...
if (shadowColor.r + bias < positionInLightSpace.z) {
...
}
...
Peter Panning
我们看上面的效果图会发现影子和物体有一段偏移量,我们一般称这种现象为Peter Panning,正如彼得·潘故事里的主人公和影子分离的现象。
想要修复这个问题,可以提高深度纹理的精度。我们在设置光源投影矩阵的时候,nearPlane和farPlane都设置的比较大,这导致了深度纹理需要表达更大范围的值,从而丧失了精度。
self.lightProjectionMatrix = GLKMatrix4MakeOrtho(-10, 10, -10, 10, -100, 100);
我们将它修改成如下数据。
self.lightProjectionMatrix = GLKMatrix4MakeOrtho(-10, 10, -10, 10, 0.1, 28);
效果就会好很多。
软阴影
现在的阴影看起来还可以,不过边缘会有些锯齿。真实的阴影边缘往往比较柔和,我们可以通过多重采样让阴影变得柔和。
if (shadowUV.x >= 0.0 && shadowUV.x <=1.0 && shadowUV.y >= 0.0 && shadowUV.y <=1.0) {
vec2 texelSize = 1.0 / vec2(1024, 1024);
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture2D(shadowMap, shadowUV + vec2(x, y) * texelSize).r;
shadow += positionInLightSpace.z - bias < pcfDepth ? 0.6 : 0.0;
}
}
shadow /= 9.0;
}
vec2(1024, 1024)
是阴影贴图的大小,你也可以通过uniform把贴图大小传递进来。通过采样相邻点的深度值,判断是否需要阴影,然后求出平均值,从而达到平滑的效果。效果如下。
处理未被阴影贴图覆盖的区域
目前我们的算法有一个瑕疵,你所看见的区域有可能不会全部被阴影贴图覆盖到,我们把下面的正方体变大就可以看到问题。floor的大小改为30,1,30。
- (void)createFloor {
UIImage *normalImage = [UIImage imageNamed:@"stoneFloor_NRM.png"];
GLKTextureInfo *normalMap = [GLKTextureLoader textureWithCGImage:normalImage.CGImage options:nil error:nil];
UIImage *diffuseImage = [UIImage imageNamed:@"stoneFloor.jpg"];
GLKTextureInfo *diffuseMap = [GLKTextureLoader textureWithCGImage:diffuseImage.CGImage options:nil error:nil];
NSString *cubeObjFile = [[NSBundle mainBundle] pathForResource:@"cube" ofType:@"obj"];
WavefrontOBJ *cube = [WavefrontOBJ objWithGLContext:self.glContext objFile:cubeObjFile diffuseMap:diffuseMap normalMap:normalMap];
cube.modelMatrix = GLKMatrix4Multiply(GLKMatrix4MakeTranslation(0, -1, 0), GLKMatrix4MakeScale(30, 1, 30 ));
[self.objects addObject:cube];
}
效果如下。未被阴影贴图覆盖的区域都是黑色的。
我们只要在Fragment Shader中添加一个分支条件就可以解决这个问题。
if (shadowUV.x >= 0.0 && shadowUV.x <=1.0 && shadowUV.y >= 0.0 && shadowUV.y <=1.0) {
vec2 texelSize = 1.0 / vec2(1024, 1024);
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture2D(shadowMap, shadowUV + vec2(x, y) * texelSize).r;
shadow += positionInLightSpace.z - bias < pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;
} else {
shadow = 1.0;
}
最后我们打开法线贴图,效果如下。
What‘s Next?
ShadowMap的两篇文章主要介绍了ShadowMap的基本原理和常见问题的修复思路,除了这些之外还有其他更加复杂的优化和解决issus的方案。想要继续深入了解,可以参考常用的ShadowMap问题解决方案,以及它的底部标明的引用的文章。
本文参考的文章列表
Shadow Mapping
Common Techniques to Improve Shadow Depth Maps