博客文章

2DShadow的生成(生成2D阴影)

作者: andy.      时间: 2019-07-14 15:29:25

    一直想写这个东西,上周完成了一半,这周把这个补完。关于2D和3D这个东西还有一个小插曲。前天晚上和老板,还有同时说皇室战争是2D做的还是3D做的。结论是什么不重要。其实现代图形学当中,已经没有纯粹的2D了,很早以前还有CPU计算2D图形这种。但是现在的图形学接口都是3D的。所以应该意义上来说,所有的游戏都是3D的,但是呢。很多2D引擎忽略了z轴,直接设置为0。那么就都是在正Z(positive z)看向x和y轴组成的平面。如果要实现一个3D效果,最简单的就是放个3D模型在上面,就看做这个3D模型的时候,他的每个面的朝向了,看看在做光照的时候要不要处理他们的法向量贴图或者视差贴图等等问题。

    好了,扯得太多了,进入正题,如何实现一个2D阴影。先上代码:

    https://github.com/andyzhangyb/OpenGLEffects/tree/master/2DShadows

    https://github.com/andyzhangyb/OpenGLEffects/tree/master/Resources/Shaders/2DShadows

    https://github.com/andyzhangyb/OpenGLEffects

    实现方式不涉及复杂的光线算法,只是通过OpenGL的图形接口来实现。如果要实现一个阴影,在3D环境中,我们需要创建一个CubeMap,计算出从光源位置看向每个方向的深度值。想象一下,有一个光源,向每个方向射出一条射线,那么,每个射线能照好远(物体挡住了)就是这个位置的深度值。我们将深度值存储在CubeMap里面。然后实际渲染的时候,我们就可以从摄像机位置看,能看到的某个位置距离光源的距离,把这个距离和光源进行对比,就可以得到他是否在阴影中了。这是3D实现阴影的方式,网上有很多文章。

    这个方式实现2D阴影和3D阴影理论是一样的,只需要把z轴的值置为0就行了。我们把光源位置设置在(0, 0)处(我们可以通过点击,改变光源的位置来看一下对应阴影的效果),先看效果:

image.png    

    实现步骤:

    1、先创建一个GL_TEXTURE_CUBE_MAP用来存从光源位置看向4个方向的深度值(因为2DShadow,所有正Z和负Z方向没有)。

unsigned int depthCubeMap;
glGenTextures(1, &depthCubeMap);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubeMap);
for (size_t i = 0; i < 6; i++)
	glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, WIDTH_SHADOW, HEIGHT_SHADOW, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
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_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubeMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

    2、渲染4个方向,取得每个位置得深度值,我们需要将点转换为每个方向进行渲染。实际上计算红色三角形的深度的时候,这里做过一点点儿处理,不然获取不到深度值或者有错误,后面会解释。Shader后面再解释。

glm::mat4 shadowProj = glm::perspective(glm::radians(90.f), (float)WIDTH_SHADOW / (float)HEIGHT_SHADOW, nearPlane, farPlane);
glm::mat4 matrix;
matrix = shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f));
shaderShadowMap.SetMat4("shadowMatrices[0]", matrix);
matrix = shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f));
shaderShadowMap.SetMat4("shadowMatrices[1]", matrix);
matrix = shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
shaderShadowMap.SetMat4("shadowMatrices[2]", matrix);
matrix = shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f));
shaderShadowMap.SetMat4("shadowMatrices[3]", matrix);

glViewport(0, 0, WIDTH_SHADOW, HEIGHT_SHADOW);
renderCube();

    3、渲染整个场景,判断每个点是否在阴影中,然后确定其是显示白色还是黑色。


    全部步骤大概就是这样子的。然后看Shader,我们在几何着色器,指定渲染4个面(z方向的两个面去掉了),这样一次调用将在光源处看向四个面,每个面的深度值算出来。

#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;

uniform mat4 shadowMatrices[4];

out vec3 FragPos;

void main() {
    for (int face = 0; face < 4; ++face) {
        gl_Layer = face;
        for (int i = 0; i < 3; ++i) {
            FragPos = gl_in[i].gl_Position.xyz;
            gl_Position = shadowMatrices[face] * gl_in[i].gl_Position;
            EmitVertex();
        }
        EndPrimitive();
    }
}

    再在片段着色器中计算出深度并存入深度缓冲:

#version 330 core
in vec3 FragPos;

uniform vec2 lightPos;
uniform float farPlane;

void main() {
    float lightDistance = length(FragPos - vec3(lightPos, 0.0));
    lightDistance = lightDistance / farPlane;
    gl_FragDepth = lightDistance;
}

    在最后渲染的时候就直接判断是否在阴影中,然后处理显示图像了:

#version 330 core
out vec4 FragColor;

in vec3 fragPos;

uniform samplerCube depthMap;

uniform vec2 lightPos;
uniform float farPlane;

uniform int drawTrueColor;

void main() {
    float lightDistance = length(fragPos - vec3(lightPos.xy, 0.0)) / farPlane;
    float minDistance = texture(depthMap, fragPos - vec3(lightPos.xy, 0.0)).r;
    if (drawTrueColor == 1)
        FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    else
        if (lightDistance > minDistance)
            FragColor = vec4(0.0, 0.0, 0.0, 1.0);
        else 
            FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    // FragColor = vec4(minDistance, minDistance, minDistance, 1.0);
}

    整体还是很简单的。和3D的思路是一样的。


    然后说几个坑的地方:

    1、三角形的深度值的计算。我们使用generateCubeBox将这个2D的三角形转成了3D的,因为2D的因为误差,采样的时候可能拿不到其深度值。想象一下,一个2D图像,朝正 z和负 z移动一点儿距离,然后组成一个立方体,具体实现看generateCubeBox函数。

    2、指定对深度贴图采样的时候如果坐标超过了他的实际坐标,处理方式为GL_CLAMP_TO_EDGE。如果不这样的话,就有毛毛躁躁的线条。可以想一哈为哈子~


    这种方式对性能的影响肯定是很小,毕竟没有复杂的算法,而且是调用的OpenGL本身的API实现。

    如果要实现soft shadow,这个也很简单了,只需要在取深度值的时候,采样围绕着点周围采,然后计算平均值。这个方法简单易懂,但是采样多了还是很影响性能。

    关于移植到游戏引擎,抽个时间看看。每天还是很忙。