前言
上篇我们谈及phone光照、法线贴图与切线空间,实现了高效的光照效果。本篇我们将基于上篇的效果实现阴影贴图添加硬阴影
原理
阴影贴图的思想是光源看不到而摄像机可以看到的地方,基于此思想我们将阴影贴图的步骤分为三步:
- 将光源看作一个摄像机进行渲染,将光源空间的深度值写入阴影映射纹理,记为\(z_{map}\),并记录当前的
投影矩阵
(点光源为透视投影,方向光为正交投影)
- 再以摄像机视角进行渲染,将屏幕空间的深度信息写入z-buffer
- 将摄像机视角的坐标乘以投影矩阵(将当前像素的屏幕空间深度转换到光源空间),得到\(z\)。再将\(z\)和\(z_{map}\)进行比较,若\(z < z_{map}\),那么认为当前片元处于阴影中
下图展示了,使用shadow map 和不使用shadow map的区别
软阴影和硬阴影
shadow map只能实现硬阴影效果,且只用使用点光源。下面展示了两种阴影的区别,上图为硬阴影,下图为软阴影
硬阴影很好理解,但软阴影是如何形成的呢?因为光源是有体积的,这会导致有的地方完全看不到光源(称为本影, Umbra), 但有的地方能看到一部分光源(称为半影,Penumbra),所以阴影的边缘会产生过渡,从而产生软阴影。比如上图中的全日食与半日食
存在的问题
-
自遮挡
原因:数值精度的限制。毕竟计算机中浮点数太难判断,这会造成z-fighting现象的出现
-
走样
原因:因为阴影贴图分辨率是有限的,而每个像素占据一定大小,且离光源越远,每个像素覆盖的片元就越多
由于解决这些问题还需要涉及到其他算法,目前我们只是实现shadow map,待以后使用dx实现实时阴影
实现
第一次PASS:输出光源的zbuffer
-
shader.h
在shader头文件添加一个深度值,该值影响最终输出的光源zbuffer的效果(可以根据需要指定)
const float depth = 500.f;
-
main.cpp
//用于输出光源的zbuffer struct DepthShader : public IShader { mat <3, 3, float> m_varyingTri; virtual Vec4f Vertex(int nFace, int nthVert) { Vec4f vertex = embed<4>(g_Model->vert(nFace, nthVert)); vertex = g_Viewport * g_Projection * g_ModelView * vertex; m_varyingTri.set_col(nthVert, proj<3>(vertex / vertex[3])); return vertex; } virtual bool fragment(Vec3f weight, TGAColor& color) { Vec3f pInter = m_varyingTri * weight; color = TGAColor(255, 255, 255) * (pInter.z / depth); return false; } }; float* g_ShadowBuffer = nullptr; g_ShadowBuffer = new float[g_Width * g_Height]; for (int i = g_Width * g_Height; --i; ) g_ZBuffer[i] = g_ShadowBuffer[i] = std::numeric_limits<float>::min(); g_LightDir.normalize(); //输出光源的zbuffer { TGAImage depth(g_Width, g_Height, TGAImage::RGB); LookAt(g_Camera, g_Target, g_Up); Projection(0); ViewPort(g_Width / 8, g_Height / 8, g_Width * 3 / 4, g_Height * 3 / 4); for (int m = 1; m < argc; ++m) { g_Model = new Model(argv[m]); DepthShader shader; Vec4f screen_coords[3]; for (int i = 0; i < g_Model->nfaces(); ++i) { for (int j : {0, 1, 2}) { screen_coords[j] = shader.Vertex(i, j); } Rasterization(screen_coords, shader, depth, g_ShadowBuffer); } delete g_Model; } depth.flip_vertically(); depth.write_tga_file("depth.tga"); }
输出:
第二次PASS:输出结果
struct ShadowMapShader : public IShader
{
mat <3, 3, float> m_varyingTri; //coordinates before Viewport transform
mat <2, 3, float> m_varyingUV; //uv coordinates
mat <4, 4, float> m_uniformM; //Projection*ModelView
mat <4, 4, float> m_uniformMIT; //(Projection*ModelView).invert_transpose()
mat <4, 4, float> m_uniformMShadow; // transform camera's viewport into light's
ShadowMapShader(Matrix m, Matrix mIT, Matrix mS) :
m_uniformM(m), m_uniformMIT(mIT), m_uniformMShadow(mS), m_varyingTri(), m_varyingUV() {}
virtual Vec4f Vertex(int nFace, int nthVert)
{
//从模型文件中获取uv,顶点坐标
m_varyingUV.set_col(nthVert, g_Model->uv(nFace, nthVert));
auto vertex = embed<4>(g_Model->vert(nFace, nthVert));
vertex = g_Viewport * g_Projection * g_ModelView * vertex;
m_varyingTri.set_col(nthVert, proj<3>(vertex / vertex[3]));
return vertex;
}
virtual bool fragment(Vec3f weight, TGAColor& color)
{
//将当前片元变换至光源空间
Vec4f shadBuffP = m_uniformMShadow * embed<4>(m_varyingTri * weight);
shadBuffP = shadBuffP / shadBuffP[3];
//计算光源buffer的索引
int index = static_cast<int>(shadBuffP[0]) + static_cast<int>(shadBuffP[1]) * g_Width;
//std::cout << index << std::endl;
double shadow = 0.3 + (g_ShadowBuffer[index] < shadBuffP[2]) * .7; //0.3是一个offset,避免z-flighting
auto uv = m_varyingUV * weight; //uv插值
auto n = proj<3>(m_uniformMIT * embed<4>(g_Model->normal(uv))).normalize(); //由模型空间变换至其他空间可能会导致法线不是等比变换,需要乘以逆转置矩阵
auto l = proj<3>(m_uniformM * embed<4>(g_LightDir)); //将光源变换至透视投影空间
auto r = (n * (n * l) * 2 - l).normalize(); //镜面反射的反射光
float spec = std::pow(std::max(r.z, 0.f), g_Model->specular(uv)); //削弱镜面光的范围
float diff = std::max(0.f, n * l); //漫反射
TGAColor c = g_Model->diffuse(uv);
for (int i : {0, 1, 2})
color[i] = std::min<float>(20 + c[i] * shadow * (1.2 * diff + 0.6 * spec), 255); //阴影相当于是一个系数
return false;
}
};
//用于变换至光源空间
auto lightM = g_Viewport * g_Projection * g_ModelView;
{
TGAImage frameBuffer(g_Width, g_Height, TGAImage::RGB); // the output image
//摄像机的变换
LookAt(g_Camera, g_Target, g_Up);
Projection(-1.f / (g_Camera - g_Target).norm());
ViewPort(g_Width / 8, g_Height / 8, g_Width * 3 / 4, g_Height * 3 / 4);
//vert f x/y/z x/y/z x/y/z : x = vertex coord, y = uv coor, z = normal coor
//face : triangle face
for (int m = 1; m < argc; ++m)
{
g_Model = new Model(argv[m]);
ShadowMapShader shader(g_ModelView, g_Projection * g_ModelView.invert_transpose(), lightM * (g_Viewport * g_Projection * g_ModelView).invert());
Vec4f screen_coords[3];
for (int i = 0; i < g_Model->nfaces(); ++i)
{
for (int j : {0, 1, 2})
{
screen_coords[j] = shader.Vertex(i, j);
}
Rasterization(screen_coords, shader, frameBuffer, g_ZBuffer);
}
delete g_Model;
}
//Image.flip_vertically();
frameBuffer.flip_vertically();
frameBuffer.write_tga_file("output.tga");
}
输出:
reference
https://github.com/ssloy/tinyrenderer/wiki/Lesson-7:-Shadow-mapping
https://games-cn.org/intro-graphics/
标签:map,int,光源,vertex,阴影,Width,从零开始,shadow,Model From: https://www.cnblogs.com/chenglixue/p/17220000.html