这一节我们来讨论阴影。
ShadowMap
我们如何判断一个点是否处于阴影中呢?
比如说在图中的ABCD四个点,B和C处于阴影中,A和D不处于阴影中,我们要做的事情就是能够把这两组点区分开来。这两组点有什么区别呢?我们把自己放在光源的位置,这就一目了然了:在光源的位置,沿着光源方向观察,A和D在视野内,B和C不在视野内,这就很容易判断出,B和C是处于阴影中的。
ShadowMap的原理就是如此,先在光源的位置,沿着光源方向渲染一张深度图,然后在实际渲染场景时,把每个点变换到光源视角下,比较其深度和深度图中对应位置深度大小,倘若其深度比深度图中的深度大,说明从光源的角度是看不到这个点的,那么这个点应该处在阴影中。反之亦然。
代码实现
首先我们要定义一块空间来存储ShadowMap,就本质来说它和深度缓冲没什么区别,我们就在初始化深度缓冲的时候初始化ShadowMap:
for (int i = mWidth * mHeight; i--; zBuffer[i] = shadowBuffer[i] = FLT_MAX);
为了渲染出ShadowMap,我们也需要一个额外的shader,为了提供必要的抽象,我们先定义一个IShader基类:
class IShader {
public:
virtual ~IShader() {};
virtual TGAColor fragment(const Vector3& bar) = 0;
bool depthPass;
};
通过继承基类,我们定义出DepthShader:
class DepthShader: public IShader {
public:
DepthShader(Matrix4* m_mvp);
~DepthShader();
Vector4 vertex(int id, const Vector4& modelPts, const Vector3& uv, const Vector4& normalDir);
TGAColor fragment(const Vector3& bar);
Matrix3 varying_uv;
Matrix4 varying_normal;
private:
DepthShader(const DepthShader&);
Matrix4 uniform_M;
Matrix4 clipCoors;
};
DepthShader的实现比较简单,因为不需要实际画出像素,Fragment里直接返回一个空值就可以了:
#include "DepthShader.h"
DepthShader::DepthShader(Matrix4* m_mvp) :uniform_M(*m_mvp)
{
depthPass = true;
}
DepthShader::~DepthShader()
{
}
Vector4 DepthShader::vertex(int id, const Vector4& modelPts, const Vector3& uv, const Vector4& normalDir)
{
Vector4 clipCoor = uniform_M * modelPts;
clipCoors.setColumn(id, clipCoor);
return clipCoor;
}
TGAColor DepthShader::fragment(const Vector3& bar)
{
TGAColor color;
return color;
}
在画模型里的每个三角形的时候,我们先用DepthShader画一遍,再用SimpleShader画一遍:
for (size_t i = 0; i < shapes.size(); i++) {
size_t index_offset = 0;
assert(shapes[i].mesh.num_face_vertices.size() == shapes[i].mesh.material_ids.size());
SimpleShader shader(&mvpMatrix, &shadowMvpMatrix, &modelMatrix, &tgaImage, lightDir, shadowBuffer);
DepthShader depthShader(&shadowMvpMatrix);
// For each face
for (size_t f = 0; f < shapes[i].mesh.num_face_vertices.size(); f++) {
size_t fnum = shapes[i].mesh.num_face_vertices[f];
Vector4 modelPts;
Vector4 clip_coords[3];
Vector4 depth_clip_coords[3];
Vector3 uv;
Vector4 normal;
Vector4 viewDir[3];
// For each vertex in the face
for (size_t v = 0; v < fnum; v++) {
tinyobj::index_t idx = shapes[i].mesh.indices[index_offset + v];
modelPts = Vector4(attrib.vertices[3 * idx.vertex_index + 0], attrib.vertices[3 * idx.vertex_index + 1], attrib.vertices[3 * idx.vertex_index + 2], 1);
uv = Vector3(attrib.texcoords[2 * idx.texcoord_index + 0], attrib.texcoords[2 * idx.texcoord_index + 1], 0);
normal = Vector4(attrib.normals[3 * idx.normal_index + 0], attrib.normals[3 * idx.normal_index + 1], attrib.normals[3 * idx.normal_index + 2],0);
viewDir[v] = cameraPos - modelMatrix * modelPts;
clip_coords[v] = shader.vertex(v, modelPts, uv, normal);
depth_clip_coords[v] = depthShader.vertex(v, modelPts, uv, normal);
}
if(!useWireFrame)
triangle(depth_clip_coords, &depthShader, shadowBuffer);
if (shader.varying_normal.getColumn(0).dot(viewDir[0]) > 0 || shader.varying_normal.getColumn(1).dot(viewDir[1]) > 0 || shader.varying_normal.getColumn(2).dot(viewDir[2]) > 0)
triangle(clip_coords, &shader, zBuffer);
index_offset += fnum;
}
}
同时我们也要改造一下SimpleShader,在Fragment里把点变换到光源视角下,然后和ShadowMap里比较来判断是否处于阴影中,如果处于阴影中,就把颜色衰减一点:
TGAColor SimpleShader::fragment(const Vector3& bar)
{
Vector3 buv = varying_uv * bar;
Vector3 bn = varying_normal * bar;
bn.normalize();
float intensity = (bn.dot(light_dir)) * 0.5f + 0.5f;
TGAColor color = tgaImage->get(buv[0] * tgaImage->get_width(), buv[1] * tgaImage->get_height());
Vector4 clipPos = varying_clip.getColumn(0) * bar.x + varying_clip.getColumn(1) * bar.y + varying_clip.getColumn(2) * bar.z;
Vector4 transformedPos = uniform_Mshadow * clipPos;
transformedPos /= transformedPos.w;
transformedPos.x = (transformedPos.x / 2 + 0.5) * SCREEN_WIDTH;
transformedPos.y = (transformedPos.y / 2 + 0.5) * SCREEN_HEIGHT;
transformedPos.z = (transformedPos.z / 2 + 0.5);
bool notInShadow = false;
if (transformedPos.x < 0 || transformedPos.x > SCREEN_WIDTH || transformedPos.y < 0 || transformedPos.y > SCREEN_HEIGHT || transformedPos.z < 0 || transformedPos.z > 1)
notInShadow = true;
else
{
int idx = (int)transformedPos.x + (int)transformedPos.y * SCREEN_WIDTH;
notInShadow = shadowBuffer[idx] > transformedPos.z - 0.008;
}
float shadow = .2 + .8 * notInShadow;
return color * intensity * shadow;
}
Shadow Acne
notInShadow = shadowBuffer[idx] > transformedPos.z - 0.008;
在计算是否在阴影中时,我们加上了一点点的偏移量,这个偏移量叫做Shadow Bias,为了看到这个偏移量的效果,我们可以先不加这个偏移量试试:
可以看到图中出现了很多条纹状的阴影,这种现象就称为Shadow Acne。
Shadow Acne产生的原因在于ShadowMap是离散的,因此多个场景中的点都对应到ShadowMap中的同一个像素。然而这些点的实际深度有的比ShadowMap中对应的深度小,有的比ShadowMap中对应的深度大,前者没有问题,后者就会出现不应该产生的阴影。为了解决这个问题,就需要手动加上一个Shadow Bias:
基于此,我们就能够得到一个正常的阴影了:
最后附上代码:
https://github.com/LittleLittleWind/TaurusSoftRenderer