《OGL dev》Etay Meiri Tutorial 23 - Shadow Mapping - Part 1 笔记
生成阴影的技术有很多种,Shadow Mapping是其中比较简单基础的一种。
一次深度测试能在depth buffer中得到与屏幕上每个像素对应的每个离摄像机最近的片元的z值。shadow mapping利用这个原理, 通过两次draw call调用,实现一个光源的阴影。其中每一次draw call调用称为一个pass。
- 第一个pass,将摄影机置于光源,得到depth buffer。这个pass不渲染到color buffer,不显示到屏幕上。而是将depth buffer的内容写入到一张纹理,称为shadow map。向这样将一次渲染的结果写入到纹理的技术称为“render to texture(渲染到纹理)”,简称RTT。
- 第二个pass,将摄影机放置回它本来的位置,正常渲染。在fragment shader中将shadow map作为纹理读入,将每一个片元变换到以光源为摄影机的NDC空间中,再将它的深度值与shadow map对应像素的深度值进行比较。如果片元的深度值与shadow map对应深度值不同,说明它不是离光源最近的片元,需要绘制阴影。如果相同,则不需绘制阴影。
Part1教程只学习第一个pass,因此第二个pass改为将shadow map渲染到屏幕上。具体OpenGL的API调用如下。
第一个pass:
首先需要创建一个纹理对象和framebuffer对象。
- 纹理对象用于保存shadow map。纹理对象,通过
glGenTextures(1, &m_shadowMap);
创建,glDeleteTextures(1, &m_shadowMap);
销毁,其中m_shadowMap是GLuint类型的handle。 - 而framebuffer对象则是因为,默认情况下,OpenGL渲染到默认的framebuffer的color buffer,从而显示到屏幕上。将OpenGL渲染的到的framebuffer改为用户创建的framebuffer对象,可以通过配置这个framebuffer,使OpenGL渲染到纹理对象,并阻止它显示在屏幕上。framebuffer对象,通过
glGenFramebuffers(1, &m_fbo);
创建,glDeleteFramebuffers(1, &m_fbo);
销毁,其中m_fbo是GLuint类型的handle。
然后初始化纹理对象和framebuffer对象。
- 初始化纹理对象。
- 绑定texture target:
glBindTexture(GL_TEXTURE_2D, m_shadowMap);
将m_shadowMap绑定到当前texture unit的GL_TEXTURE_2D。(默认texture unit为texture unit 0)。 - 初始化纹理数据:
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
- 第一个参数为要操作的texture target。
- 第二个为mipmap level参数,0表示最高分辨率。
- 第三个表示OpenGL中存储纹理数据的格式,GL_DEPTH_COMPONENT表示归一化单浮点深度。
- WindowWidth和WindowHeight表示纹理的宽高。
- 之后的0是border(边界)参数,目前不使用。
- 之后的GL_DEPTH_COMPONENT表示内存中纹理数据的格式。
- 之后的GL_FLOAT,表示每个通道为单精度浮点。
- 最后是NULL是内存中的数据源,因为shadow map是从渲染写入的,赋值初始数据源没有意义。
- 设定filter,不设置的话是无法对浮点纹理坐标进行采样的。
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
将放大和缩小都设定为线性过滤(即双线性过滤)。 - 设定纹理坐标超过[0, 1]区间的情况如何采样。这是可选的,默认值为GL_REPEAT,表示重复。GL_CLAMP_TO_EDGE表示超出部分使用边界颜色。
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
分别设置u和v坐标。
- 绑定texture target:
- 初始化framebuffer对象。
- 绑定:
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
,将m_fbo绑定到GL_FRAMEBUFFER,后续对GL_FRAMEBUFFER的操作,应用于m_fbo。- glBindFramebuffer的第一个参数可选GL_FRAMEBUFFER、GL_DRAW_FRAMEBUFFER或GL_READ_FRAMEBUFFER。其中GL_FRAMEBUFFER表示读写,GL_DRAW_FRAMEBUFFER表示写入,GL_READ_FRAMEBUFFER表示读取。GL_FRAMEBUFFER是初始化framebuffer对象时推荐的方式。渲染时会使用GL_DRAW_FRAMEBUFFER。使用glReadPixels函数时,会用到GL_READ_FRAMEBUFFER(教程目前不使用)。
glBindFramebuffer(GL_FRAMEBUFFER, 0);
绑定到默认的framebuffer。
- 将纹理对象附加到framebuffer对象:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap, 0);
,GL_DEPTH_ATTACHMENT表示附加到depth attachment point,纹理对象将用于接收深度测试的结果,即depth buffer。attachment point还支持以下选项:- COLOR_ATTACHMENTi,纹理对象将接收fragment shader的颜色,即color buffer。i为整数后辍,因为fragment shader可以同时渲染到多个color buffer。
- STENCIL_ATTACHMENT,纹理对象将接收模版测试的结果,即stencil buffer。
- DEPTH_STENCIL_ATTACHMENT,纹理对象将接收depth buffer和stencil buffer的组合,因两者常一起使用。
- 可选 ,
glDrawBuffer(GL_NONE);
,参数可选COLOR_ATTACHMENTi,默认color buffer附加到GL_COLOR_ATTACHMENT0。GL_NONE表示不写入到color buffer,同时禁用fragment shader。- 默认的framebuffer的选项只能是GL_NONE、GL_FRONT_LEFT、GL_FRONT_RIGHT、GL_BACK_LEFT和GL_BACK_RIGHT其中之一,FRONT和BACK分别对应front和back buffer,LEFT和RIGHT是因为每个buffer都有left和right buffer。
- 可选,
glReadBuffer(GL_NONE);
禁用framebuffer读取,这是为了避免仅支持OpenGL 3.x而不支持4.x的GPU出问题。 - 错误检查:
GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
,Status != GL_FRAMEBUFFER_COMPLETE表示发生错误。
- 绑定:
最后渲染:
- 绑定:
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
- 清空depth buffer:
glClear(GL_DEPTH_BUFFER_BIT);
- 以光源作为摄影机调用draw call。
- 绑定到默认framebuffer:
glBindFramebuffer(GL_FRAMEBUFFER, 0);
,因为第二个pass要渲染到color buffer。
第二个Pass:
- 清空color buffer和depth buffer:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
- 激活texture unit 0,并将shadow map绑定到它的2D纹理,以供fragment shader中读取。
- 正常摄影机调用draw call。
教程中的fragment shader代码Depth = 1.0 - (1.0 - Depth) * 25.0;
是因为以光源为摄像机时,由于透视投影矩阵与物体的位置设定,shadow map的深度值接近于1,直接输出到color buffer颜色会非常淡,因为rgb为0为黑,为1为白。因此对从shadow map的深度值到1这段区域,向深度值为0的方向,进行了25倍缩放,以使得颜色更深。