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,不能乘以透视矩阵,因为透视矩阵不能作用于光线。