Computer Graphics(7)

Shadow Mapping阴影贴图

基本思路

阴影贴图的思想较为简单:首先选择光源所在的位置为视角进行渲染,按照阴影产生的原理,我们所能看到的东西能被点亮,而反之看不到的部分则处在阴影之中。
容易想到的解决思路是:对光源发出的射线上的点进行遍历,并记录第一个与物体相交的点。如果在这条光射线上的点比这个交点距离光源的距离更远,那么较远的点处在阴影之中。
但是在渲染过程中逐一对不同方向上的射线、同一射线上的无数个点进行计算比较显然是不切实际的,所以考虑开启深度测试,使用深度测试的方法来简化实现的过程。
这里我们考虑从光源的透视图来渲染场景,并将深度值的结果存储在纹理之中——也就是说,对光源的透视图所见的最近的深度值进行采样,所得到的这个深度值就是我们在光源的角度下透视图能够见到的第一个片元。所有的这些深度值被称作深度贴图(depth map)。
有了深度贴图之后,我们可以在渲染原有基本场景的基础上直接使用深度贴图来计算片元是否需要调整成阴影即可。

STEP1深度贴图的获取

  • 帧缓冲的概念
    在前面的作业中,我们用到的屏幕缓冲有很多:用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件来丢弃特定片段的模板缓冲。所有的这些缓冲结合起来叫做帧缓冲(Frame Buffer)。
    个人理解:帧缓冲可以看做是某一帧对应的所有信息的合集。而在我们前面实现的场景中,我们是在默认的帧缓冲上进行的,如果设置了自己的帧缓冲,那么我们可以直接对已经存在的场景进行处理(而不是需要重新建立符合新要求的场景)。
    帧缓冲的具体实现方式参考教程。
  • 准备工作
    我们需要将深度贴图存储在一个纹理中来用于后续对于阴影的计算,所以首先我们为深度贴图建立一个帧缓冲对象(FBO):
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

然后我们为这个帧缓冲创建一些附件,并将这些附件附加到帧缓冲上。
首先是纹理附件。按照作业四中的方法创建一个纹理,不同之处在于此次实验中我们只关心深度值,所以将纹理的格式指定为GL_DEPTH_COMPONENT,然后将生成的深度纹理作为帧缓冲的深度缓冲附加到帧缓冲上:

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
// 纹理附加
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
// 在这次作业中,我们需要的仅仅是深度缓冲,颜色缓冲是没有必要的
// 所以需要设置下面两个语句来告诉OpenGL我们不使用任何颜色数据进行渲染
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
  • 生成深度贴图(render loop中的处理)
    使用深度贴图来渲染场景。与前面的处理结合,这里分两步进行渲染:首先渲染深度贴图,然后使用深度贴图渲染场景:
// 从光源的角度渲染深度缓冲
simpleDepthShader.use();
simpleDepthShader.setMat4("lightSpaceMatrix", lightSpaceMatrix);
// viewpoint设置!!
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
// 关于depthMap深度贴图的着色器内容设置,见STEP2
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
renderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// 重置视点
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 利用深度贴图渲染场景
// --------------------------------------------------------------
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.use();
glm::mat4 projection = glm::perspective(camera.Zoom, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
...// 设置shader
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, woodTexture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, depthMap);
renderScene(shader);
  • 光源设置
    这里是对光源空间的变换。用来确保为物体选择了合适的投影(正交/透视)和视图矩阵(使用lookat获得)。
    首先是对于光线的选择:(bonus1内容)
// 透视光线,通常用于点光源、舞台光线等。同时注意如果选择使用透视光线,光源对应的着色器也需要进行相应的处理
if (perspective) {
    Shader debugDepthQuad("debug_quad.vs.txt", "debug_quad_perspective.fs.txt");
    lightProjection = glm::perspective(glm::radians(45.0f)*10, (GLfloat)SHADOW_WIDTH / (GLfloat)SHADOW_HEIGHT, near_plane, far_plane);
    ortho = false;
}
// 正交投影光线,用于平行光线的设置,如太阳光等
if (ortho) {
    lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
    perspective = false;
}

接着,由于我们需要从光源的角度来观察场景,那么视图矩阵可以采用lookat函数获取:

glm::mat4 lightView = glm::lookAt(glm::vec(-2.0f, 4.0f, -1.0f), glm::vec3(0.0f), glm::vec3(1.0));

二者相乘就得到了我们在光源空间内变换需要的矩阵:glm::mat4 lightSpaceMatrix = lightProjection * lightView;

STEP2渲染至深度贴图(shader source的处理)

在STEP1中,我们已经知道了如何得到深度贴图,并且知道了利用光的透视图(深度贴图)渲染的大致过程。下面是我们在渲染过程中需要用到的着色器的设置。

  • 将顶点变换到光源空间(对应深度贴图获取过程中的渲染深度缓冲)
    即对cube的顶点进行lightSpaceMatrix变换,使其变换到光源空间,以便进一步获取深度贴图。所以在顶点着色器中对顶点位置处理即可;而由于这里仅仅是对深度进行处理,所以片段着色器设置为空即可:
// vertex shader
#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
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D depthMap;
uniform float near_plane;
uniform float far_plane;
// 透视投影时用到,将非线性的深度值转换为线性的,从而得到易于观察的深度值
float LinearizeDepth(float depth)
{
    float z = depth * 2.0 - 1.0; // Back to NDC 
    return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));    
}
void main()
{             
    float depthValue = texture(depthMap, TexCoords).r;
    // FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // 透视
    FragColor = vec4(vec3(depthValue), 1.0); // 正交
}

STEP3渲染阴影

STEP1、2已经得到了完整的深度贴图,接下来就是生成阴影的步骤,显然依旧是在着色器(shader mapping对应的着色器)中对场景进行处理。
首先是在顶点着色器中,我们需要对场景中的片元判断其是否在阴影之中,实现如下:

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);

    vs_out.FragPos = vec3(model * vec4(position, 1.0));
    vs_out.Normal = transpose(inverse(mat3(model))) * normal;
    vs_out.TexCoords = texCoords;
    // 变换到光源空间的坐标   
    vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);
}

而对于片段着色器,我们选择Phong模型进行渲染。对于阴影部分,我们设置一个shadow值,如果片元在阴影内则shadow=1,反之则shadow=0。回忆上次作业中,Phong模型中主要的光照分量是环境光、漫反射光和镜面反射光,显然这里我们需要将阴影对应的系数shadow与漫反射分量和镜面反射分量相乘即可。
但是在实际实现的过程中,我们需要对shadow的值进行更细致的处理,这里放在函数ShadowCalculation中,实现如下:

float ShadowCalculation(vec4 fragPosLightSpace)
{
    // 透视除法,对于正交投影没有任何影响
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    // normalize
    projCoords = projCoords * 0.5 + 0.5;
    // 获取光源坐标下最近的深度值
    float closestDepth = texture(shadowMap, projCoords.xy).r; 
    // 获取当前片元在光源坐标下的深度值
    float currentDepth = projCoords.z;



    // 越界处理
    if(projCoords.z > 1.0)
        shadow = 0.0;

    return shadow;
}

然后将这个shadow系数添加到lighting结果的计算中去即可:

float shadow = ShadowCalculation(fs_in.FragPosLightSpace);       
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color; 

STEP4阴影优化

  • 针对阴影中的线条
    这种情况是由于多个片元从同一个深度值采样所造成的。比如当多个片元从同一个斜坡的深度纹理像素中采样时,有的在地板上有的在地板下,也就是说有的片元被认为在阴影中,有的不在,于是产生了条纹。可以通过添加偏移值处理:
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;
  • 针对场景一半明亮一半暗
    这是由于采样过多和坐标超出光的正交视锥的结果。可以通过储存一个边框颜色,然后将深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER来解决采样过多的问题:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

而对于第二种情况,可以通过检查边界来优化。在上面我们已经处理过。

Bonus

1、分别实现正交、透视投影下的shadow mapping

在前面已经实现。具体需要修改的地方是render loop中添加对投影方式的选择,以及shadow mapping对应片段着色器中着色方式。

2、阴影优化

  • 对阴影锯齿边的简单处理
    PCF的思路是从深度贴图中多次取样,然后对不同的结果进行平均即可得到较柔和的阴影,实现如下:
float shadow = 0.0;
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;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要下载计算机图形学,需要采取以下步骤: 首先,确定你需要下载的计算机图形学软件或工具。计算机图形学是一个广泛的领域,包括各种不同的软件和工具,如3D建模软件、动画制作工具、渲染引擎等。根据你的需求和兴趣,选择合适的软件或工具进行下载。 其次,你可以通过互联网搜索引擎来查找和筛选可靠的下载来源。你可以输入关键词"计算机图形学软件下载"或"计算机图形学工具下载"来搜索相关的结果。在搜索结果中,你可以浏览不同的网站、论坛或软件下载平台,找到适合你的软件或工具。 在下载之前,需要确定下载来源的可靠性和安全性。首先,选择官方网站或知名的软件下载平台来获取软件或工具,以确保下载的软件是正版和安全的。其次,阅读其他用户的评价和反馈,了解软件的质量和性能,以便做出明智的决策。 一旦确定下载来源,点击下载链接,并根据指引完成下载过程。下载的速度取决于你的互联网连接和文件大小。一般来说,下载较大的计算机图形学软件可能需要一些时间,所以请耐心等待。 下载完成后,你可以打开下载的文件进行安装。根据软件或工具的具体要求,进行相应的安装步骤。一般来说,安装过程包括双击安装文件、选择安装路径、确认安装选项等。 最后,一旦安装完成,你就可以开始使用计算机图形学软件或工具了。根据你的需求,你可以学习使用软件的各种功能和工具,进行各种计算机图形学相关的任务,如建模、动画制作、渲染等。 总的来说,下载计算机图形学软件或工具是一个简单的过程,你只需要确定你的需求,选择可靠的下载来源,进行下载和安装即可。随着你的学习和实践,你可以更好地掌握计算机图形学的技术和应用。 ### 回答2: 计算机图形学是一个涉及到计算机科学与艺术的领域,旨在使用计算机生成、编辑和显示图像。所谓的“computer graphics下载”可能指的是下载计算机图形学的相关软件或教程。关于这个问题,以下是一个可能的回答: 下载计算机图形学软件和教程可以帮助我们学习和应用计算机图形学的相关知识和技术。在互联网上,我们可以通过搜索引擎查找计算机图形学软件的官方网站或其他下载渠道,从中下载我们所需要的软件。常见的计算机图形学软件包括Photoshop、3ds Max、Maya等,它们分别用于图像处理、三维建模和动画制作等方面。 同时,我们还可以在各种相关的技术论坛、学术网站或在线教育平台上找到计算机图形学的教程和学习资料。这些教程包括基础知识介绍、算法实现、图形渲染技术等内容,可以帮助我们系统学习和掌握计算机图形学的理论和实践技巧。 通过下载计算机图形学软件和教程,我们可以在自己的电脑上进行图像处理、三维建模、动画设计等各种计算机图形学任务。这些任务可以应用于游戏开发、电影制作、工程设计、虚拟现实等领域。通过学习和应用计算机图形学,我们可以达到创作出精美的图像和影像的目的,同时也为我们的职业发展提供更多机会和选择。因此,计算机图形学的下载对于我们学习和实践相关领域都具有重要意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值