7.0 Lession 7 阴影渲染
[TOC]
Lession6的不足
回想一下Lesson6的冯氏光照模型,看L6_Shader.cpp
的代码, 我们最后实现的镜面效果就是根据冯氏光照模型模拟的。
最后的
Vec3f r = (n*(n*l*2.f) - l).normalize(); // reflected light
float spec = pow(std::max(r.z, 0.0f), 4);
float diff = std::max(0.f, n*l);
TGAColor c = model->diffuse(uv);
color = c;
for (int i=0; i<3; i++) {
//5 for the ambient component, 1 for the diffuse component and .6 for the specular component.
color[i] = std::min<float>(20 + c[i]*(1.2*diff + .9*spec), 255);
}
实际上,我们使用了常数环境光,加上漫反射,再加上镜面发射。得到的结果有
可以看到虽然比之前更逼真了一些,但是光不对,没有阴影,尤其是图片脖子左侧的位置。如果加上阴影我们想得到的是这样的结果
阴影实现原理
回想我们做z-buffer的时候实际建立了一个缓冲区,用于记录深度信息,我们这里也实际建立两个相同大小,相同原理的缓冲区,做两次遍历。第一次缓冲区记录光照方向、相同的ModelView变换下的深度信息,第二次使用第一次的深度信息,如果深度值小区缓冲区说明被引用覆盖。
第一次
创建DepthShader
struct DepthShader{
Vec4f m2v(Matrix m) {
return Vec4f(m[0][0], m[1][0], m[2][0],m[3][0]);
}
Vec3f varying_tri[3];
Vec4f vertex4f(int iface, int nthvert) {
Vec3f vertex = model->vert(iface, nthvert);
Vec4f gl_vertex(vertex.x, vertex.y, vertex.z, 1);
gl_vertex = m2v(Viewport * Projection * ModelView * gl_vertex);
varying_tri[nthvert] = projectDivision(gl_vertex);
return gl_vertex;
}
bool fragment(Vec3f bar, TGAColor &color) {
Vec3f p = Vec3f(
varying_tri[0].x * bar.x + varying_tri[1].x * bar.y +varying_tri[2].x * bar.z,
varying_tri[0].y * bar.x + varying_tri[1].y * bar.y +varying_tri[2].y * bar.z,
varying_tri[0].z * bar.x + varying_tri[1].z * bar.y +varying_tri[2].z * bar.z
);
color = TGAColor(255, 255, 255)*(p.z/depth);
return false;
}
};
{ // rendering the 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.vertex4f(i, j);
if (i == 0) {
std::cerr<<"screen_coords = " << screen_coords[j]<<std::endl;
}
}
triangle_shadow(screen_coords, depthshader, depth, shadowbuffer);
}
depth.flip_vertically(); // to place the origin in the bottom left corner of the image
depth.write_tga_file("lession7_depth.tga");
}
注意这里,eye的位置放在了光线上,并且没有做透视变换,实际就是Model、View的变换。
lookat(light_dir, center, up);
projection(0);
画三角形的时候实际上对shadowbuffer记录了当前视角下的深度信息
int frag_depth = z/w;
if (c.x<0 || c.y<0 || c.z<0 || zbuffer[(int)(P.x+P.y*image.get_width())]>frag_depth) continue;
bool discard = shader.fragment(c, color);
if (!discard) {
zbuffer[(int)(P.x+P.y*image.get_width())] = frag_depth;
image.set(P.x, P.y, color);
}
第二次
第二次的shader真正的开始了图片渲染,第一次我们只是记录从光照看模型的角度下的深度值。
这一次我们调整我们想要的视角和透视。
lookat(eye, center, up);
viewport(width/8, height/8, width*3/4, height*3/4);
projection(-1.f/(eye-center).norm());
Shader计算顶点坐标还是按照MVPViewport的顺序计算。
计算片元的坐标,我们需要转换到第一次缓冲区的位置,但是我们第二次的时候做了MVPViewport的计算,所以我们要先乘以第二次MVPViewPort矩阵的逆矩阵还原打局部空间,然后再乘以上一次的MVPViewPort矩阵,转换成第一次的位置。
Vec4f sb_p = m2v(uniform_Mshadow* Vec4f(p.x, p.y, p.z, 1)); // corresponding point in the shadow buffer
sb_p = sb_p/sb_p[3];
int idx = int(sb_p[0]) + int(sb_p[1])*width; // index in the shadowbuffer array
//神奇的z-fighting
float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2]);
如果当前的深度值比第一次的深度缓冲小,说明不会被照亮,那么此像素不会被照亮,颜色变暗。
struct ShadowShader{
Vec4f m2v(Matrix m) {
return Vec4f(m[0][0], m[1][0], m[2][0],m[3][0]);
}
Matrix uniform_M; // ModelView
Matrix uniform_MIT; // (Projection*ModelView).inverse().transpose()
Matrix uniform_Mshadow; // M*(Viewport*Projection*ModelView).inverse(), transform framebuffer screen coordinates to shadowbuffer screen coordinates
Vec2i varying_uv[3]; // triangle uv coordinates, written by the vertex shader, read by the fragment shader
Vec3f varying_tri[3]; // triangle coordinates before Viewport transform, written by VS, read by FS
ShadowShader(Matrix M, Matrix MIT, Matrix MS) : uniform_M(M), uniform_MIT(MIT), uniform_Mshadow(MS), varying_uv(), varying_tri() {}
Vec4f vertex(int iface, int nthvert) {
varying_uv[nthvert] = model->uv(iface, nthvert);
Vec3f vertex = model->vert(iface, nthvert);
Vec4f gl_vertex(vertex.x, vertex.y, vertex.z, 1);
gl_vertex = m2v(Viewport * Projection * ModelView * gl_vertex);
varying_tri[nthvert] = projectDivision(gl_vertex);
return gl_vertex;
}
bool fragment(Vec3f bar, TGAColor &color) {
Vec3f p = Vec3f(
varying_tri[0].x * bar.x + varying_tri[1].x * bar.y +varying_tri[2].x * bar.z,
varying_tri[0].y * bar.x + varying_tri[1].y * bar.y +varying_tri[2].y * bar.z,
varying_tri[0].z * bar.x + varying_tri[1].z * bar.y +varying_tri[2].z * bar.z
);
Vec4f sb_p = m2v(uniform_Mshadow* Vec4f(p.x, p.y, p.z, 1)); // corresponding point in the shadow buffer
sb_p = sb_p/sb_p[3];
int idx = int(sb_p[0]) + int(sb_p[1])*width; // index in the shadowbuffer array
//神奇的z-fighting
float shadow = .3+.7*(shadowbuffer[idx]<sb_p[2] +43.34);
Vec2i uv = varying_uv[0] * bar.x + varying_uv[1] * bar.y + varying_uv[2] * bar.z;
Vec3f normal = model->norm(uv);
Vec4f nt = m2v(uniform_MIT * Vec4f(normal.x, normal.y, normal.z, 1));
Vec3f n = Vec3f(nt.x, nt.y, nt.z).normalize();
//法向量不能随便使用矩阵转换空间,尤其是发生scale的情况下,要使用矩阵的逆矩阵转置矩阵
//转到世界空间
Vec4f lt = m2v((uniform_M * Vec4f(light_dir.x, light_dir.y, light_dir.z, 1)));
Vec3f l =Vec3f(lt.x, lt.y, lt.z).normalize(); // light vector
Vec3f r = (n*(n*l*2.f) - l).normalize(); // reflected light
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>(20 + c[i]*shadow*(1.2*diff + .6*spec), 255);
return false;
}
};
{ // 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());
ShadowShader shader(ModelView, (Projection*ModelView).inverse().transpose(), M*(Viewport*Projection*ModelView).inverse());
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_final(screen_coords, shader, frame, zbuffer);
}
frame.flip_vertically(); // to place the origin in the bottom left corner of the image
frame.write_tga_file("lession7_final.tga");
}
注意事项
-
注意这里,eye的位置放在了光线的方向位置上,并且没有做透视变换,实际就是Model、View的变换。
-
第一次计算深度缓冲,我们把eye放在光线的位置上,只进行ModelView 和Viewport 的计算。
-
第二次的时候需要进行正常的MVPViewPort的计算。
-
第二次进行光线计算的时候法向量的空间变化只能时候MVP的逆矩阵的转置矩阵,最后进行归一化,因为只是一个向量,所以抛弃了w元素。
Vec3f normal = model->norm(uv);
Vec4f nt = m2v(uniform_MIT * Vec4f(normal.x, normal.y, normal.z, 1));
Vec3f n = Vec3f(nt.x, nt.y, nt.z).normalize();
(Projection*ModelView).inverse().transpose()
- 第二次光线的空间变化只能× ModelView,不能乘以透视矩阵,因为透视矩阵不能作用于光线。