学习OpenGL ES之ShadowMap(二)

作者: handyTOOL | 来源:发表于2017-07-19 16:36 被阅读370次

    本系列所有文章目录

    获取示例代码


    本文将为大家解决上一篇文章留下来的问题。

    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

    相关文章

      网友评论

        本文标题:学习OpenGL ES之ShadowMap(二)

        本文链接:https://www.haomeiwen.com/subject/xcmhkxtx.html