TinyRenderer 学习笔记05:阴影映射
注意:这里谈论的是硬阴影,软阴影的计算是另一回事。
思路
阴影的解决方案实际上很简单,我们只需要进行两次渲染:
- 第一次渲染,我们在光源位置渲染图像,得到决定哪里被照亮的信息。
- 第二次渲染,我们根据第一次得到的可见性信息进行渲染。
第一次渲染(DepthShader)
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()
中调取它是这样的方式:
// 渲染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。
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;
}
};
这里面需要解释的就是这段代码:
// 影子缓冲区中的相应点
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
来转换它,然后我们就知道了阴影缓冲空间的
主函数里调用:
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分辨率不足以获得精确的结果。
一个蛮力的解决方案是:
float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]+43.34);
只需将一个zbuffer相对于另一个zbuffer移动一点,就足以删除伪影。但这会产生其他问题,不过通常问题不太明显。
结果: