注意:这里谈论的是硬阴影,软阴影的计算是另一回事。
思路 阴影的解决方案实际上很简单,我们只需要进行两次渲染:
第一次渲染,我们在光源位置渲染图像,得到决定哪里被照亮的信息。
第二次渲染,我们根据第一次得到的可见性信息进行渲染。
第一次渲染(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) { 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 { 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{ mat<4 , 4 , float > uniform_M; mat<4 , 4 , float > uniform_MIT; mat<4 , 4 , float > uniform_Mshadow; 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 ]); 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; { 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 (); 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移动一点,就足以删除伪影。但这会产生其他问题,不过通常问题不太明显。
结果: