【OpenGL】阴影映射实现实时阴影

完整代码已上传:https://github.com/kaiwu119/BallFreeFallAnimation

阴影是光线被物体遮挡而产生的,当光线照射不到物体表面时,这个物体就处于阴影中了, 阴影的存在可以让场景更加真实,而且更容易分辨出物体的相对位置。现在有很多阴影的实现方法,但是都不是那么容易实现的,而阴影映射是比较容易实现的,这次主要介绍阴影映射实现实时阴影,下面先看效果图

还是比较懒,素材选用了之前的自由落体小程序,但是对之前的效果进行了修改,可以很明显的看出现在渲染出的效果更好。

我们从上面的图看出小球的影子是实时跟着小球移动的,附加了阴影之后这个小球是不是更具有真实感了呢。

阴影映射

先说一下阴影贴图的好处:不需要了解场景中的物体,对于每个灯光而言,只需要一张纹理来保存阴影信息。

缺点是 易走样。后面会讲到。

阴影映射是实现阴影的一种方法,这种方法思路非常简单,简单来说:光源发出的一条射线中与光线触碰到的第一个物体,那么这个就是光亮点,也是在这个射线中距离光源的最近点,然后我们把这个最近点和射线上的其余点进行比较,只要比最近点远那就是处于阴影中。但是我们遍历所有的射线显然效率是很低的,于是我们引入了一种类似的举措,但是效率上更可观——深度缓冲。在深度缓冲中,里面的一个值对应于在摄像机的视角下的一个片元的深度值(取值为0~1),那么我们就可以简单的获取这些最近点的深度值了,我们只需要把摄像机架在光源的位置对屏幕进行渲染,然后把获取到的深度值保存到一张纹理中,这个纹理就叫做深度贴图,或是阴影贴图。之后我们正常渲染的时候,我们就可以让每一个片元的深度值和阴影贴图相对应的深度值比较,我们就能知道这个片元是否处于阴影之中。

那么如何比较呢?因为我们的阴影贴图是在光源的位置上获取的,那么我们同样也要将所有的片元转换到光源空间中,然后再进行比较。

技术路线是:

  1.  创建阴影贴图
  2.  渲染场景到阴影贴图
  3.  正常渲染场景(使用阴影贴图)

下面我们分别具体阐述方法和代码:

 

1. 创建阴影贴图

    glGenFramebuffers(1, depthMapFBO); //生成一个帧缓冲对象

    glGenTextures(1, depthMap);
    glBindTexture(GL_TEXTURE_2D, *depthMap);//绑定深度图
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
                 SHADOW_WIDTH, SHADOW_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);
    //把生成的深度纹理作为帧缓冲的深度缓冲
    glBindFramebuffer(GL_FRAMEBUFFER, *depthMapFBO);//激活深度图的帧缓冲
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, *depthMap, 0);
    //设置不用任何颜色
    glDrawBuffer(GL_NONE);
    glReadBuffer(GL_NONE);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

这里我们用了帧缓冲,在默认情况下我们目前所做的所有操作都是在默认帧缓冲的渲染缓冲上进行的。OpenGL允许我们定义我们自己的帧缓冲,也就是说我们可以指定渲染物体到我们的帧缓冲中。这里我们把生成的阴影贴图作为帧缓冲的深度缓冲因为我们只要深度,所以说我们设置不使用颜色缓冲。

首先我们正常的生成一个帧缓冲对象和纹理对象,纹理格式指定为GL_DEPTH_COMPONENT,NULL表示只分配空间,glBindFramebuffer(GL_FRAMEBUFFER, 0);中的0表示我们重新绑定到默认缓冲。这里我们的SHADOW_WIDTH, SHADOW_HEIGHT均取1024;表明贴图的大小

2. 渲染场景到阴影贴图

    glViewport(0, 0, 1024,1024);
    glBindFramebuffer(GL_FRAMEBUFFER, *depthMapFBO);
    //    if(core->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
    //        qDebug()<<"depthBuffer ok";
    glClear(GL_DEPTH_BUFFER_BIT);

    Model.setToIdentity();
    QMatrix4x4 lightView;
    lightView.lookAt(QVector3D(eyePos),QVector3D(0,0,0),QVector3D(0,1,0));//设置摄像机
    QMatrix4x4 lightProjection;
    lightProjection.ortho(left,right,bottom,top,near,far);//设置正交矩阵
    QMatrix4x4 lightSpaceMatrix = lightProjection * lightView;

   
    depthShader->setUniformValue("lightSpaceMatrix",lightSpaceMatrix);
    Model.translate(x,y,z);
    depthShader->setUniformValue("model",Model);

    drawSomething();
    //绑定默认帧缓冲
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

这里我们先设置好窗口大小 1024x1024,这是因为这个要和阴影贴图的尺寸保持一致,然后我们绑定好我们自己的帧缓冲,清空缓冲中的深度缓冲,然后开始定义一些矩阵,主要是定义lightSpaceMatrix(转换到光照空间)。这里还要注意的是我们使用的投影矩阵是正交矩阵,因为我们是使用的平行光。之后绘制场景到当前帧缓冲后,别忘了把帧缓冲改回默认的帧缓冲,否则后面的绘制会出现异常。这里采用的着色器非常简单:

顶点着色器:只是将点转换到光照空间中

#version 330 core
layout (location = 0) in vec3 position;

uniform mat4 lightSpaceMatrix;
uniform mat4 model;

void main()
{
    gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f);
}

片元着色器:由于我们没有颜色缓冲,最后的片元不需要任何处理,所以我们可以简单地使用一个空片元着色器

#version 330 core

void main()
{             
    // gl_FragDepth = gl_FragCoord.z;
}


3.正常渲染场景

这个就不多说了,主要是这次要将我们获得好的阴影贴图传入我们正常绘制物体的着色器中

下面看正常渲染的着色器

顶点着色器

顶点着色器主要是将信息(顶点坐标,法线,纹理坐标,在光照空间的点坐标)传给片元着色器。

#version 330 core
uniform mat4 m_projection;
uniform mat4 m_view;
uniform mat4 m_model;
uniform mat4 lightSpaceMatrix;

in vec3 a_position;
in vec3 a_normal;
in vec3 a_texcoord;

out VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
    vec4 FragPosLightSpace;
} vs_out;

void main()
{
    gl_Position = m_projection*m_view*m_model*vec4(a_position,1.0);
	
	vs_out.FragPos = vec3(m_model * vec4(a_position, 1.0));
    vs_out.Normal = transpose(inverse(mat3(m_model))) * a_normal; //model矩阵的逆矩阵的转置矩阵消除了缩放操作对法线的影响
    vs_out.TexCoords = a_texcoord.xy;
    vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);

}

片元着色器

下面主要讲一下片元着色器的内容: 

首先要检查一个片元是否在阴影中,把光空间片元位置转换为裁切空间的标准化设备坐标。当我们在顶点着色器输出一个裁切空间顶点位置到gl_Position时,OpenGL自动进行一个透视除法,将裁切空间坐标的范围-w到w转为-1到1,这要将x、y、z元素除以向量的w元素来实现。由于裁切空间的FragPosLightSpace并不会通过gl_Position传到像素着色器里,我们必须自己做透视除法
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;


因为我们要取得纹理坐标(0~1)而且深度z也是(0~1)所以我们要将点projCoords 变换到[0,1]的范围
    projCoords = projCoords * 0.5 + 0.5;
    float closestDepth = texture(shadowMap, projCoords.xy).r; 


然后我们取得当前片元在光源视角下的深度
    float currentDepth = projCoords.z;

当前深度和最近深度进行比较

    float shadow = currentDepth > closestDepth  ? 1.0 : 0.0;

然后加上光照计算就会得到阴影效果了。

 

但是如果直接比较会得到下面的结果:

很明显有锯齿,失真,因为深度图1024*1024会受限于解析度,所以在远处的片元有可能从深度贴图采用同一个值进行深度对比对于具有梯度的物体,如果相邻两片元从深度图获得同一深度d,但是事实上两者的currentDepth是不同的,就会造成一个在表面,一个在表面下(认为是阴影),我们可以让解析度更大解决这个问题,也可以采用一个偏移量来进行更正:
    float bias = 0.005;

    float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;

用bias让每个片元的深度均在表面上。更加可靠的办法能够根据表面朝向光线的角度更改偏移量

使用点乘 bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

这样我们就会的到下面这种结果:

这时候物体表面没有锯齿了,但是阴影还是有锯齿。

我们用一种简单的方法产生柔和阴影:从纹理像素四周对深度贴图采样,然后把结果平均起来;

然后我们获得最终的效果;

完整片元着色器如下 

#version 330 core
uniform vec3 viewPos;
uniform vec3 lPos;


in VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
    vec4 FragPosLightSpace;
} fs_in;

uniform sampler2D texture0;
uniform sampler2D shadowMap;
out vec4 f_color;



vec3 CalLight(vec3 lightPos,vec3 lightColor,float ambient,float specular,float shadow)//漫反射系数,镜面反射系数,shadow若不用阴影则设为-1
{
    //vec3 norm = calBumpedNormal();
    vec3 norm = fs_in.Normal;
    vec3 ambientLight = ambient*lightColor;

    vec3 lightDir = normalize(lightPos - fs_in.FragPos);

    float diffuse = max(dot(norm, lightDir),0.0);

    vec3 diffuseLight = diffuse*lightColor;

    vec3 viewDir = normalize(viewPos - fs_in.FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);

    vec3 specularLight = specular * spec * lightColor;
	
    if(shadow < 0)
    return (ambientLight+diffuseLight+specularLight);
	else
	return (ambientLight + (1.0 - shadow) * (diffuseLight + specularLight)) * lightColor;  
}

float ShadowCalculation(vec4 fragPosLightSpace)
{
    // 执行透视除法
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    // 变换到[0,1]的范围
    projCoords = projCoords * 0.5 + 0.5;
    // 取得最近点的深度(使用[0,1]范围下的fragPosLight当坐标)
    float closestDepth = texture(shadowMap, projCoords.xy).r; 
    // 取得当前片元在光源视角下的深度
    float currentDepth = projCoords.z;

	vec3 normal = fs_in.Normal;
	vec3 lightDir = normalize(lPos - fs_in.FragPos);
	float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

	
	//进行光滑
	float shadow = 0.0;
	//这个textureSize返回一个给定采样器纹理的0级mipmap的vec2类型的宽和高。用1除以它返回一个单独纹理像素的大小,我们用以对纹理坐标进行偏移,确保每个新样本,来自不同的深度值。
	vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
	for(int x = -1; x <= 1; ++x)
	{
		for(int y = -1; y <= 1; ++y)
		{
        float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; 
        shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;        
		}    
	}
	shadow /= 9.0;

    return shadow;
}
void main()
{

  	
	vec3 lightPos = lPos;
	vec3 color = texture(texture0, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);
    vec3 lightColor = vec3(1.0);
 	
 // 计算阴影

    float shadow = ShadowCalculation(fs_in.FragPosLightSpace); 
    vec3 lighting = CalLight(lightPos,lightColor,0.3,0.2,shadow) ;

    f_color = vec4(lighting*color,1.0);
	
}




 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页