0%

TinyRenderer学习笔记05:阴影映射

注意:这里谈论的是硬阴影,软阴影的计算是另一回事。

思路

阴影的解决方案实际上很简单,我们只需要进行两次渲染:

  • 第一次渲染,我们在光源位置渲染图像,得到决定哪里被照亮的信息。
  • 第二次渲染,我们根据第一次得到的可见性信息进行渲染。

第一次渲染(DepthShader)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct DepthShader : public IShader
{
mat<3, 3, float> varying_tri;

DepthShader() : varying_tri() {}

virtual Vec4f vertex(int iface, int nthvert)
{
// 从OBJ文件中读取顶点数据
Vec4f gl_Vertex = embed<4>(model->vert(iface, nthvert));
// 将其转换为屏幕坐标
gl_Vertex = Viewport * Projection * ModelView * gl_Vertex;
varying_tri.set_col(nthvert, proj<3>(gl_Vertex / gl_Vertex[3]));
return gl_Vertex;
}

virtual bool fragment(Vec3f bar, TGAColor &color)
{
Vec3f p = varying_tri * bar;
color = TGAColor(255, 255, 255) * (p.z / depth);
return false;
}
};

这个Shader的工作很简单,就是将zbuffer的信息复制到framebuffer中,从main()中调取它是这样的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 渲染Shadow-buffer
{
TGAImage depth(width, height, TGAImage::RGB);
lookat(light_dir, center, up);
viewport(width / 8, height / 8, width * 3 / 4, height * 3 / 4);
projection(0);

DepthShader depthshader;
Vec4f screen_coords[3];
for (int i = 0; i < model->nfaces(); i++)
{
for (int j = 0; j < 3; j++)
{
screen_coords[j] = depthshader.vertex(i, j);
}
triangle(screen_coords, depthshader, depth, shadowbuffer);
}
depth.flip_vertically();
depth.write_tga_file("depth.tga");
}

它的渲染结果:

第二次渲染(Shader)

第二次渲染用到的Shader就是我们之前做的那个,只不过需要多乘上一个shadow。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct Shader : public IShader
{
// Projection*ModelView
mat<4, 4, float> uniform_M;
// (Projection*ModelView).invert_transpose()
mat<4, 4, float> uniform_MIT;
// 将帧缓冲区的屏幕坐标转换为阴影缓冲区的屏幕坐标
mat<4, 4, float> uniform_Mshadow;
// 三角形的uv坐标,由顶点着色器写入,由片段着色器读取
mat<2, 3, float> varying_uv;
// 视口变换前的三角坐标,由顶点着色器写入,由片段着色器读取
mat<3, 3, float> varying_tri;

Shader(Matrix M, Matrix MIT, Matrix MS) : uniform_M(M), uniform_MIT(MIT), uniform_Mshadow(MS), varying_uv(), varying_tri() {}

virtual Vec4f vertex(int iface, int nthvert)
{
varying_uv.set_col(nthvert, model->uv(iface, nthvert));
Vec4f gl_Vertex = Viewport * Projection * ModelView * embed<4>(model->vert(iface, nthvert));
varying_tri.set_col(nthvert, proj<3>(gl_Vertex / gl_Vertex[3]));
return gl_Vertex;
}

virtual bool fragment(Vec3f bar, TGAColor &color)
{
// 影子缓冲区中的相应点
Vec4f sb_p = uniform_Mshadow * embed<4>(varying_tri * bar);
sb_p = sb_p / sb_p[3];
// 影子缓冲器阵列中的索引
int idx = int(sb_p[0]) + int(sb_p[1]) * width;
float shadow = .3 + .7 * (shadowbuffer[idx] < sb_p[2]);
// 为当前像素插值uv
Vec2f uv = varying_uv * bar;
// 法向量
Vec3f n = proj<3>(uniform_MIT * embed<4>(model->normal(uv))).normalize();
// 光向量
Vec3f l = proj<3>(uniform_M * embed<4>(light_dir)).normalize();
// 反射光向量
Vec3f r = (n * (n * l * 2.f) - l).normalize();
float spec = pow(std::max(r.z, 0.0f), model->specular(uv));
float diff = std::max(0.f, n * l);
TGAColor c = model->diffuse(uv);
for (int i = 0; i < 3; i++)
color[i] = std::min<float>(5 + c[i] * shadow * (1 * diff + 2 * spec), 255);
// 我们不丢弃这个像素
return false;
}
};

这里面需要解释的就是这段代码:

1
2
3
4
5
6
// 影子缓冲区中的相应点
Vec4f sb_p = uniform_Mshadow * embed<4>(varying_tri * bar);
sb_p = sb_p / sb_p[3];
// 影子缓冲器阵列中的索引
int idx = int(sb_p[0]) + int(sb_p[1]) * width;
float shadow = .3 + .7 * (shadowbuffer[idx] < sb_p[2]);

在之前声明了一个矩阵mat<4,4,float> uniform_Mshadow,它允许将当前片段的屏幕坐标转换为阴影缓冲区内的屏幕坐标。

varying_tri*bar为我提供了我们当前绘制的像素的屏幕坐标;我们用1来提升它(类似齐次坐标里我们所做的),然后用神奇的矩阵uniform_Mshadow来转换它,然后我们就知道了阴影缓冲空间的$xyz$坐标。现在要确定当前像素是否被点亮,只需将其$z$坐标与我们存储在shadowbuffer中的值进行比较。

主函数里调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Matrix M = Viewport*Projection*ModelView;

{ // rendering the frame buffer
TGAImage frame(width, height, TGAImage::RGB);
lookat(eye, center, up);
viewport(width/8, height/8, width*3/4, height*3/4);
projection(-1.f/(eye-center).norm());

Shader shader(ModelView, (Projection*ModelView).invert_transpose(), M*(Viewport*Projection*ModelView).invert());
Vec4f screen_coords[3];
for (int i=0; i<model->nfaces(); i++) {
for (int j=0; j<3; j++) {
screen_coords[j] = shader.vertex(i, j);
}
triangle(screen_coords, shader, frame, zbuffer);
}
frame.flip_vertically(); // to place the origin in the bottom left corner of the image
frame.write_tga_file("framebuffer.tga");
}

矩阵M是从对象空间到shadowbuffer屏幕空间的转换矩阵。我们将相机返回到其正常位置,重新计算Viewport矩阵、Projection矩阵,并调用第二个着色器。

我们知道Viewport*Projection*ModelView将物体的坐标转换到 (framebuffer) 屏幕空间。我们需要知道如何将framebuffer screen转换为shadow screen。其实很简单:(Viewport*Projection*ModelView).invert()可以将framebuffer的坐标转换成物体坐标,然后M*(Viewport*Projection*ModelView).invert()给出了framebuffer和shadowbuffer之间的转换。

结果:

这里你可能会发现结果上有一些奇怪的线条,这就是所谓的Z-fighting。我们的buffer分辨率不足以获得精确的结果。

一个蛮力的解决方案是:

1
float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]+43.34);

只需将一个zbuffer相对于另一个zbuffer移动一点,就足以删除伪影。但这会产生其他问题,不过通常问题不太明显。

结果: