从零开始做一个软渲染器——视锥剔除、齐次裁剪和背面剔除
从零开始做一个软渲染器——视锥剔除、齐次裁剪和背面剔除
项目地址:https://github.com/DogWealth/PIRenderer
1. 视锥剔除
最简单的视锥剔除只需要在齐次空间(也就是透视投影变换之后,透视除法之前)中对三角形的顶点判断是否满足如下条件
\[-w\le x \le w \\
-w\le y \le w \\
-w\le z \le w
\]
如果有顶点不满足这个条件,可以直接简单粗暴的把整个三角形给剔除掉。
bool Renderer::AllInsidePlane(const V2F& v1, const V2F& v2, const V2F& v3)
{
if (v1.m_ScreenPos.x > v1.m_ScreenPos.w || v1.m_ScreenPos.x < -v1.m_ScreenPos.w) return false;
if (v1.m_ScreenPos.y > v1.m_ScreenPos.w || v1.m_ScreenPos.y < -v1.m_ScreenPos.w) return false;
if (v1.m_ScreenPos.z > v1.m_ScreenPos.w || v1.m_ScreenPos.z < -v1.m_ScreenPos.w) return false;
if (v2.m_ScreenPos.x > v2.m_ScreenPos.w || v2.m_ScreenPos.x < -v2.m_ScreenPos.w) return false;
if (v2.m_ScreenPos.y > v2.m_ScreenPos.w || v2.m_ScreenPos.y < -v2.m_ScreenPos.w) return false;
if (v2.m_ScreenPos.z > v2.m_ScreenPos.w || v2.m_ScreenPos.z < -v2.m_ScreenPos.w) return false;
if (v3.m_ScreenPos.x > v3.m_ScreenPos.w || v3.m_ScreenPos.x < -v3.m_ScreenPos.w) return false;
if (v3.m_ScreenPos.y > v3.m_ScreenPos.w || v3.m_ScreenPos.y < -v3.m_ScreenPos.w) return false;
if (v3.m_ScreenPos.z > v3.m_ScreenPos.w || v3.m_ScreenPos.z < -v3.m_ScreenPos.w) return false;
return true;
}
void Renderer::DrawMesh(Mesh* mesh)
{
const std::vector<Vertex>& vertexs = mesh->GetVertexBuffer();
if (vertexs.size() == 0) return;
for (int i = 0; i < vertexs.size(); i += 3)
{
V2F v1 = m_Shader->VertexShader(vertexs[i]);
V2F v2 = m_Shader->VertexShader(vertexs[i + 1]);
V2F v3 = m_Shader->VertexShader(vertexs[i + 2]);
//视锥剔除
if (!AllInsidePlane(v1, v2, v3)) continue;
PerspectiveDivision(&v1);
PerspectiveDivision(&v2);
PerspectiveDivision(&v3);
//back face culling
if (!FaceCulling(v1.m_ScreenPos, v2.m_ScreenPos, v3.m_ScreenPos)) continue;
ViewPort(&v1.m_ScreenPos);
ViewPort(&v2.m_ScreenPos);
ViewPort(&v3.m_ScreenPos);
DrawTriangle(&v1, &v2, &v3);
}
}
效果如下图,过度很不自然,整个地板突然就消失了
2. 齐次裁剪
更好的做法是对超出视锥体的三角形进行裁剪,裁剪后会生成更多的三角形。
原理参考下面的文章:
bool Renderer::InsidePlane(const Vector3f& plane, const Vector3f& pos)
{
return (plane.x * pos.x + plane.y * pos.y + plane.z * pos.z + plane.w * pos.w) >= 0;
}
bool Renderer::AllInsidePlane(const V2F& v1, const V2F& v2, const V2F& v3)
{
if (v1.m_ScreenPos.x > v1.m_ScreenPos.w || v1.m_ScreenPos.x < -v1.m_ScreenPos.w) return false;
if (v1.m_ScreenPos.y > v1.m_ScreenPos.w || v1.m_ScreenPos.y < -v1.m_ScreenPos.w) return false;
if (v1.m_ScreenPos.z > v1.m_ScreenPos.w || v1.m_ScreenPos.z < -v1.m_ScreenPos.w) return false;
if (v2.m_ScreenPos.x > v2.m_ScreenPos.w || v2.m_ScreenPos.x < -v2.m_ScreenPos.w) return false;
if (v2.m_ScreenPos.y > v2.m_ScreenPos.w || v2.m_ScreenPos.y < -v2.m_ScreenPos.w) return false;
if (v2.m_ScreenPos.z > v2.m_ScreenPos.w || v2.m_ScreenPos.z < -v2.m_ScreenPos.w) return false;
if (v3.m_ScreenPos.x > v3.m_ScreenPos.w || v3.m_ScreenPos.x < -v3.m_ScreenPos.w) return false;
if (v3.m_ScreenPos.y > v3.m_ScreenPos.w || v3.m_ScreenPos.y < -v3.m_ScreenPos.w) return false;
if (v3.m_ScreenPos.z > v3.m_ScreenPos.w || v3.m_ScreenPos.z < -v3.m_ScreenPos.w) return false;
return true;
}
V2F Renderer::Intersect(const V2F& v1, const V2F& v2, const Vector3f& plane)
{
float d1 = v1.m_ScreenPos.x * plane.x +
v1.m_ScreenPos.y * plane.y +
v1.m_ScreenPos.z * plane.z +
v1.m_ScreenPos.w * plane.w;
float d2 = v2.m_ScreenPos.x * plane.x +
v2.m_ScreenPos.y * plane.y +
v2.m_ScreenPos.z * plane.z +
v2.m_ScreenPos.w * plane.w;
float t = d1 / (d1 - d2);
V2F v;
V2F::Interpolate(&v, v1, v2, t);
return v;
}
std::vector<V2F> Renderer::SutherlandHodgeman(const V2F& v1, const V2F& v2, const V2F& v3)
{
std::vector<V2F> output = { v1, v2, v3 };
if (AllInsidePlane(v1, v2, v3)) return output;
for (auto& plane : Viewplanes)
{
std::vector<V2F> input = output;
output.clear();
for (int i = 0; i < input.size(); i++)
{
V2F cur = input[i];
V2F nex = input[(i + 1) % input.size()];
if (InsidePlane(plane, cur.m_ScreenPos))
{
output.push_back(input[i]);
if (!InsidePlane(plane, nex.m_ScreenPos))
{
output.push_back(Intersect(cur, nex, plane));
}
}
else if (InsidePlane(plane, nex.m_ScreenPos))
{
output.push_back(Intersect(cur, nex, plane));
}
}
}
return output;
}
void Renderer::DrawMesh(Mesh* mesh)
{
const std::vector<Vertex>& vertexs = mesh->GetVertexBuffer();
if (vertexs.size() == 0) return;
for (int i = 0; i < vertexs.size(); i += 3)
{
V2F v1 = m_Shader->VertexShader(vertexs[i]);
V2F v2 = m_Shader->VertexShader(vertexs[i + 1]);
V2F v3 = m_Shader->VertexShader(vertexs[i + 2]);
//视锥剔除,齐次裁剪
auto vertices = SutherlandHodgeman(v1, v2, v3);
for (int j = 0; j < int(vertices.size() - 2); j++)
{
v1 = vertices[0];
v2 = vertices[j + 1];
v3 = vertices[j + 2];
PerspectiveDivision(&v1);
PerspectiveDivision(&v2);
PerspectiveDivision(&v3);
//back face culling
if (!FaceCulling(v1.m_ScreenPos, v2.m_ScreenPos, v3.m_ScreenPos)) continue;
ViewPort(&v1.m_ScreenPos);
ViewPort(&v2.m_ScreenPos);
ViewPort(&v3.m_ScreenPos);
DrawTriangle(&v1, &v2, &v3);
}
}
}
最终效果:
需要注意vector中生成点的排列顺序很重要,不然乱序的三角形的节点会影响三角形的绘制以及背面剔除,如下所示:
3. 背面剔除
参考文章:
//背面剔除
bool Renderer::FaceCulling(const Vector3f& v1, const Vector3f v2, const Vector3f v3)
{
Vector3f v12 = v2 - v1;
Vector3f v13 = v3 - v1;
Vector3f view = { 0, 0, 1 };
Vector3f normal = Vector3f::CrossProduct(v12, v13);
return (normal * view) > 0;
}
开启背面剔除后帧率稳定在80FPS作用,不开的时候70FPS