从零开始做一个软渲染器——视锥剔除、齐次裁剪和背面剔除

从零开始做一个软渲染器——视锥剔除、齐次裁剪和背面剔除

项目地址: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);
    }
}

效果如下图,过度很不自然,整个地板突然就消失了

culling.gif

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);
        }
    }
}

最终效果:

Homogeneous_clipping.gif

需要注意vector中生成点的排列顺序很重要,不然乱序的三角形的节点会影响三角形的绘制以及背面剔除,如下所示:

Homogeneous_clipping_err.gif

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

posted @ 2024-03-04 23:22  DogWealth~  阅读(236)  评论(0编辑  收藏  举报