这几天花了点时间优化了一下阴影。
原本的阴影实现有一些比较严重的问题:
第一个是我使用了透视投影,再加上角度问题,导致阴影看起来存在近大远小的变形,不符合太阳光产生的阴影;
第二个是我的阴影贴图是在世界空间的固定位置生成的,且阴影贴图的大小是有限的,导致整个画面中,只有落在阴影贴图中的非常小的一部分物体能够产生阴影;如果扩大阴影的视锥体范围,则可以容纳更多的物体,但这会导致深度精度降低,阴影质量大幅下降。
为了避免阴影只能在某个区域产生,首先需要做的改动是在相机前生成阴影区域(fit to view)。比较理想的位置是阴影贴图的区域恰好覆盖整个视锥体,但是这个实际上不太现实,因为为了显式更多的物体,视锥体的远裁剪面一般都会非常大。因此我们优先选择比较靠近相机位置的地方作为灯光空间的原点。
我尝试着做了第一版(只有一个靠近相机的阴影贴图)阴影优化,此时确实解决了只在世界空间某个位置能生成阴影的问题,但是仍然存在一个弊端,只要相机稍微拉远一点,阴影就消失了。
为了保证阴影能覆盖到更多区域,需要考虑使用层级阴影。通俗而言,就是在视锥体不同深度的位置分别生成阴影贴图,从而覆盖更多区域。此时,随着镜头推动,我们切换使用不同的阴影贴图,阴影精度也在发生改变。镜头比较远的时候,阴影将消失不显示,也比较符合lod的概念。
计算灯光空间变化矩阵、灯光视锥体
我们把视锥体(上图为截面)分为多个区域,其中圆点为相机位置,射线为太阳光方向。图中我们为三个区域分别生成shadow map。
对于太阳光而言,我们使用正交投影来生成阴影。那么灯光视锥体的形状将为一个立方体,一个比较好的选取方法就是在灯光空间取区域的AABB包围盒。比如,对于橙色区域而言,我们选取的包围盒为:
为了能够在灯光空间求得视锥体的AABB包围盒,首先我们需要求出视锥体区域的八个顶点的世界坐标:
计算的一个比较简单的方法是,在相机空间中,利用相似三角形先求得每个顶点的坐标,然后将它们转换到世界空间。如下代码中,将视锥体分为了四个区域,共五个截面,分别计算每个截面的顶点坐标(此处zPos就是相机空间下的多个z值,注意在OpenGL坐标系里,相机前物体的z坐标都是负数):
void Camera::UpdateVertex()
{
float fov = RenderCommon::Inst()->GetFov();
float aspect = RenderCommon::Inst()->GetAspect();
QMatrix4x4 inverted_viewMatirx = viewMatrix.inverted();
// leftTop, rightTop, leftBottom, rightBottom
float tan_fov = tan(fov/2);
vector<float> zPos{0, 15, 30, 100, 200};
for(size_t i = 0;i < zPos.size(); i++)
{
float z = zPos[i];
float y = z * tan_fov;
float x = y * aspect;
z = -z;
QVector4D tmp_leftTopPos = QVector4D(-x, y, z, 1);
QVector4D tmp_rightTopPos = QVector4D(x, y, z, 1);
QVector4D tmp_leftBottomPos = QVector4D(-x, -y, z, 1);
QVector4D tmp_rightBottomPos = QVector4D(x, -y, z, 1);
tmp_leftTopPos = inverted_viewMatirx * tmp_leftTopPos;
tmp_rightTopPos = inverted_viewMatirx * tmp_rightTopPos;
tmp_leftBottomPos = inverted_viewMatirx * tmp_leftBottomPos;
tmp_rightBottomPos = inverted_viewMatirx * tmp_rightBottomPos;
viewPos[i][0] = QVector3D(tmp_leftTopPos.x(),tmp_leftTopPos.y(),tmp_leftTopPos.z()) / tmp_leftTopPos.w();
viewPos[i][1] = QVector3D(tmp_rightTopPos.x(),tmp_rightTopPos.y(),tmp_rightTopPos.z()) / tmp_rightTopPos.w();
viewPos[i][2] = QVector3D(tmp_leftBottomPos.x(),tmp_leftBottomPos.y(),tmp_leftBottomPos.z()) / tmp_leftBottomPos.w();
viewPos[i][3] = QVector3D(tmp_rightBottomPos.x(),tmp_rightBottomPos.y(),tmp_rightBottomPos.z()) / tmp_rightBottomPos.w();
}
}
然后将其转换到灯光空间中。我们先构造一个转换矩阵。其中向量N代表的就是太阳光的方向。为了构造一个右手坐标系的转换矩阵,我们需要把N取反,也就是说,实际上以下代码构造的太阳光方向为(0, -1, -1)
void RenderCommon::UpdateLightSpace()
{
QVector3D upDir(0, 1, 0);
QVector3D N = QVector3D(0, 1, 1);
QVector3D U = QVector3D::crossProduct(upDir, N);
QVector3D V = QVector3D::crossProduct(N, U);
N.normalize();
U.normalize();
V.normalize();
lightSpace.setRow(0, {U.x(), U.y(), U.z(), 0}); // x
lightSpace.setRow(1, {V.x(), V.y(), V.z(), 0}); // y
lightSpace.setRow(2, {N.x(), N.y(), N.z(), 0}); // z
lightSpace.setRow(3, {0, 0, 0, 1});
}
接下来,对于每个区域,我们需要构造两个矩阵。一个是灯光空间变换矩阵,另一个是灯光的视锥体。对于前者而言,它和刚才求得的lightSpace比较类型,但lightSpace不需要考虑位移变换,所以选取了原点即可。而灯光空间变换矩阵则需要我们确定灯光的位置,并将其作为原点。对于后者而言,如前面所提到的,我们需要使用正交投影。
我们首先将视锥体的几个顶点转换到灯光空间,然后直接计算Xmin, Xmax, Ymin, Ymax, Zmin, Zmax,从而得到包围盒的八个顶点的位置。此时,我们将灯光的位置选定在这个包围盒((Xmin + Xmax) / 2, (Ymin + Ymax) / 2, Xmax)的位置。注意这个坐标是灯光空间下的,因此我们还需要将其转回到世界空间,此时我们就求出了灯光的坐标。
同时,我们也得到了视锥体的长宽分别为Xmax - Xmin, Ymax - Ymin。就这样构造完成了我们所需要的几个矩阵。
void RenderCommon::UpdateLightMatrix()
{
QVector3D upDir(0, 1, 0);
QVector3D N = QVector3D(0, 1, 1);
QVector3D U = QVector3D::crossProduct(upDir, N);
QVector3D V = QVector3D::crossProduct(N, U);
N.normalize();
U.normalize();
V.normalize();
QMatrix4x4 lightSpace_inverted = lightSpace.inverted();
for(size_t i = 0;i < Shadow_Layer; i++)
{
QVector3D frontLeftTop = Camera::Inst()->GetViewPos(i, 0);
QVector3D frontRightTop = Camera::Inst()->GetViewPos(i, 1);
QVector3D frontLeftBottom = Camera::Inst()->GetViewPos(i, 2);
QVector3D frontRightBottom = Camera::Inst()->GetViewPos(i, 3);
QVector3D backLeftTop = Camera::Inst()->GetViewPos(i + 1, 0);
QVector3D backRightTop = Camera::Inst()->GetViewPos(i + 1, 1);
QVector3D backLeftBottom = Camera::Inst()->GetViewPos(i + 1, 2);
QVector3D backRightBottom = Camera::Inst()->GetViewPos(i + 1, 3);
QVector4D frontLeftTop1 = lightSpace * QVector4D(frontLeftTop, 1);
QVector4D frontRightTop1 = lightSpace * QVector4D(frontRightTop, 1);
QVector4D frontLeftBottom1 = lightSpace * QVector4D(frontLeftBottom, 1);
QVector4D frontRightBottom1 = lightSpace * QVector4D(frontRightBottom, 1);
QVector4D backLeftTop1 = lightSpace * QVector4D(backLeftTop, 1);
QVector4D backRightTop1 = lightSpace * QVector4D(backRightTop, 1);
QVector4D backLeftBottom1 = lightSpace * QVector4D(backLeftBottom, 1);
QVector4D backRightBottom1 = lightSpace * QVector4D(backRightBottom, 1);
frontLeftTop = QVector3D(frontLeftTop1.x(),frontLeftTop1.y(),frontLeftTop1.z()) / frontLeftTop1.w();
frontRightTop = QVector3D(frontRightTop1.x(),frontRightTop1.y(),frontRightTop1.z()) / frontRightTop1.w();
frontLeftBottom = QVector3D(frontLeftBottom1.x(),frontLeftBottom1.y(),frontLeftBottom1.z()) / frontLeftBottom1.w();
frontRightBottom = QVector3D(frontRightBottom1.x(),frontRightBottom1.y(),frontRightBottom1.z()) / frontRightBottom1.w();
backLeftTop = QVector3D(backLeftTop1.x(),backLeftTop1.y(),backLeftTop1.z()) / backLeftTop1.w();
backRightTop = QVector3D(backRightTop1.x(),backRightTop1.y(),backRightTop1.z()) / backRightTop1.w();
backLeftBottom = QVector3D(backLeftBottom1.x(),backLeftBottom1.y(),backLeftBottom1.z()) / backLeftBottom1.w();
backRightBottom = QVector3D(backRightBottom1.x(),backRightBottom1.y(),backRightBottom1.z()) / backRightBottom1.w();
float x1 = min({frontLeftTop.x(),frontRightTop.x(), frontLeftBottom.x(), frontRightBottom.x(),
backLeftTop.x(), backRightTop.x(), backLeftBottom.x(), backRightBottom.x() });
float x2 = max({frontLeftTop.x(),frontRightTop.x(), frontLeftBottom.x(), frontRightBottom.x(),
backLeftTop.x(), backRightTop.x(), backLeftBottom.x(), backRightBottom.x() });
float y1 = min({frontLeftTop.y(),frontRightTop.y(), frontLeftBottom.y(), frontRightBottom.y(),
backLeftTop.y(), backRightTop.y(), backLeftBottom.y(), backRightBottom.y() });
float y2 = max({frontLeftTop.y(),frontRightTop.y(), frontLeftBottom.y(), frontRightBottom.y(),
backLeftTop.y(), backRightTop.y(), backLeftBottom.y(), backRightBottom.y() });
float z1 = min({frontLeftTop.z(),frontRightTop.z(), frontLeftBottom.z(), frontRightBottom.z(),
backLeftTop.z(), backRightTop.z(), backLeftBottom.z(), backRightBottom.z() });
float z2 = max({frontLeftTop.z(),frontRightTop.z(), frontLeftBottom.z(), frontRightBottom.z(),
backLeftTop.z(), backRightTop.z(), backLeftBottom.z(), backRightBottom.z() });
float center_x = (x1 + x2) / 2;
float center_y = (y1 + y2) / 2;
QVector3D pos(center_x, center_y, z2);
QVector4D tmp = lightSpace_inverted * QVector4D(pos.x(), pos.y(), pos.z(), 1);
pos = QVector3D(tmp.x(), tmp.y(), tmp.z()) / tmp.w();
lightMatrix[i].setRow(0, {U.x(), U.y(), U.z(), -QVector3D::dotProduct(U, pos)}); // x
lightMatrix[i].setRow(1, {V.x(), V.y(), V.z(), -QVector3D::dotProduct(V, pos)}); // y
lightMatrix[i].setRow(2, {N.x(), N.y(), N.z(), -QVector3D::dotProduct(N, pos)}); // z
lightMatrix[i].setRow(3, {0, 0, 0, 1});
orthoZFar[i] = z2 - z1;
lightOrtho[i].setToIdentity();
// qDebug() << orthoZFar;
lightOrtho[i].ortho(x1 - center_x, x2 - center_x, y1 - center_y, y2 - center_y, 0, orthoZFar[i]);
}
}
生成阴影映射
接下来,我们执行多次阴影映射步骤。此处我使用了四个区域,因此也需要绑定四个shader函数,引用四个fragment shader,这个地方不得不说,OpenGL写起来还是不如DirectX的effect框架好用的:
void Object::GenShadowMap()
{
for(int i = 0;i < Shadow_Layer; i++)
{
GenShadowMap(i);
}
}
void Object::GenShadowMap(int i)
{
QOpenGLFunctions* gl = QOpenGLContext::currentContext()->functions();
int screenX = RenderCommon::Inst()->GetScreenX();
int screenY = RenderCommon::Inst()->GetScreenY();
string name = "ShadowMap" + to_string(i + 1);
QOpenGLShaderProgram* shadowMapProgram = CResourceInfo::Inst()->GetProgram(name);
shadowMapProgram->bind();
auto shadowMapFrameBuffer = CResourceInfo::Inst()->CreateFrameBuffer("ShadowMap", screenX, screenY);
gl->glActiveTexture(GL_TEXTURE0);
gl->glBindTexture(GL_TEXTURE_2D, shadowMapFrameBuffer->vecTexId[0]);
shadowMapProgram->setUniformValue("ShadowMap", 0);
shadowMapProgram->setUniformValue("LightMatrix", RenderCommon::Inst()->GetLightMatrix(i));
shadowMapProgram->setUniformValue("OrthoMatrix", RenderCommon::Inst()->GetOrthoMatrix(i));
shadowMapProgram->setUniformValue("zFar", RenderCommon::Inst()->GetOrthoZFarPlane(i));
shadowMapProgram->setUniformValue("ModelMatrix", modelMatrix);
shadowMapProgram->setUniformValue("IT_ModelMatrix", IT_modelMatrix);
Draw(shadowMapProgram);
}
void ObjectInfo::Render()
{
int screenX = RenderCommon::Inst()->GetScreenX();
int screenY = RenderCommon::Inst()->GetScreenY();
// 更新位置
glClearColor(1,1,1,1);
for(const auto& obj : vecObjs)
{
obj->UpdateLocation();
}
// 阴影绘制
auto shadowMapFrameBuffer = CResourceInfo::Inst()->CreateFrameBuffer("ShadowMap", screenX, screenY);
glBindFramebuffer(GL_FRAMEBUFFER, shadowMapFrameBuffer->frameBuffer);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
for(const auto& obj : vecObjs)
{
if(obj->m_bRender && obj->m_bCastShadow)
{
obj->GenShadowMap();
}
}
// ... others
}
shadowmap.vsh
#version 450 core
uniform mat4 ProjectMatrix;
uniform mat4 LightMatrix;
uniform mat4 ModelMatrix;
in vec4 a_position;
in vec2 a_texcoord;
out vec2 v_texcoord;
out vec2 v_depth;
void main()
{
v_texcoord = a_texcoord;
gl_Position = ModelMatrix * a_position;
gl_Position = LightMatrix * gl_Position;
v_depth = gl_Position.zw;
gl_Position = ProjectMatrix * gl_Position;
}
shadowmap1.fsh
这里我用了四个fsh,分别写入四个通道里的,这里就不一一列出了。这里还传入了ShadowMap参数是为了避免结果被多次写入覆盖了。
#version 450
uniform sampler2D ShadowMap;
uniform float zFar;
in vec2 v_depth;
in vec2 v_texcoord;
out vec4 fragColor;
void main()
{
float fColor = -(v_depth.x/v_depth.y)/zFar;
fragColor.x = fColor;
fragColor.y = texture(ShadowMap, v_texcoord).y;
fragColor.z = texture(ShadowMap, v_texcoord).z;
fragColor.w = texture(ShadowMap, v_texcoord).w;
}
后处理计算阴影
计算阴影的时候,我们需要根据当前的深度,到不同的shadowmap采样:
float depth_layer[5] = {0.0, 15.0/zFar, 30.0/zFar, 100.0/zFar, 200.0/zFar};
float GetShadow(vec4 worldPos, float depth)
{
float fShadow = 0.0;
int idx = 4;
for(int i = 0;i < 4;i++)
{
if(depth >= depth_layer[i] && depth < depth_layer[i + 1])
{
idx = i;
break;
}
}
mat4 LightMatrix;
mat4 OrthoMatrix;
float orthoZFar;
if(idx == 0)
{
LightMatrix = LightMatrix1;
OrthoMatrix = OrthoMatrix1;
orthoZFar = orthoZFar1;
}
else if(idx == 1)
{
LightMatrix = LightMatrix2;
OrthoMatrix = OrthoMatrix2;
orthoZFar = orthoZFar2;
}
else if(idx == 2)
{
LightMatrix = LightMatrix3;
OrthoMatrix = OrthoMatrix3;
orthoZFar = orthoZFar3;
}
else if(idx == 3)
{
LightMatrix = LightMatrix4;
OrthoMatrix = OrthoMatrix4;
orthoZFar = orthoZFar4;
}
else
{
return 0.8;
}
vec4 lightPos = (LightMatrix * (worldPos));
float fDistance = -lightPos.z / orthoZFar;
lightPos = OrthoMatrix * lightPos;
vec2 uv = lightPos.xy / lightPos.w * 0.5 + vec2(0.5, 0.5);
uv.x = clamp(uv.x, 0, 1);
uv.y = clamp(uv.y, 0, 1);
float offset = 0.4 / orthoZFar;
for(int i = -1; i <= 1; i++)
{
for(int j = -1; j <= 1; j++)
{
float fDistanceMap;
if(idx == 0) fDistanceMap = texture(ShadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY)).x;
else if(idx == 1) fDistanceMap = texture(ShadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY)).y;
else if(idx == 2) fDistanceMap = texture(ShadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY)).z;
else if(idx == 3) fDistanceMap = texture(ShadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY)).w;
fShadow += fDistance - offset > fDistanceMap ? 0.2 : 0.8;
}
}
fShadow /= 9.0;
return fShadow;
}