系列教程4: 光栅化和Z-Buffer

本文介绍了3D渲染中的光栅化算法和Z-Buffer技术。通过光栅化算法填充三角形,利用Z-Buffer解决前后景物体遮挡问题。文章详细阐述了算法逻辑,并提供了相应的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

系列教程4: 光栅化和Z-Buffer

 

我们跳过教程3,因为教程3主要是教我们加载Blender导出的模型网格。我们暂时不需要去做这些工作。依然以cube为例足以。

 

到目前为止,我们的渲染函数可以进行简单的线框渲染。我们现在开始看看如何用光栅化算法来填充三角形。然后,我们将看到如何用Z-buffer来避免将模型背面的面绘制到前面。

 

 

光栅化

有太多不同类型的光栅化算法了。我们将在这个教程实现一个简单但是有效的光栅化算法。由于我们在CPU上运行我们的3D Soft Engine,我们必须对这部分有更多的关注。的确,它会消耗大量的CPU。而这重要的部分在今天都是由GPU来完成。

 

让我们开始一个练习。拿一张纸,开始画你能想到的所有三角形的类型。这个主意是为了找到一个通用的方法来绘制任意类型的三角形。

 

如果我们对三个顶点以y坐标的方式进行排序,即P1P2P3。我们最后只会有2种可能:


你会看到2种可能:P2在P1P3的右边 、 P2在P1P3的左边。在我们这个例子中,我们想要总是从左到右来绘制我们的线,从sx到ex。

 

此外,我们会通过移动P1.Y到P3.Y来绘制从左到右的线条。但是我们需要改变我们的逻辑当到达P2的时候,因为在这2种情况中,斜率都发生了改变。这就是为什么我们需要2步来完成绘制这个三角形。从上往下从P1.Y到P2.Y,然后从P2.Y到P3.Y,我们最终的目标。

 

如何完成我们的算法所需要理解的逻辑都在维基百科中: http://en.wikipedia.org/wiki/Slope 

这真的只是一些基础的数学知识。

 

为了对case1case2进行排序,你需要简单的计算一下斜率的倒数:

dP1P2 = P2.X - P1.X / P2.Y - P1.Y and dP1P3 = P3.X - P1.X / P3.Y - P1.Y

 

如果dP1P2 > dP1P3 则是第一个例子,即P2在右边。否则如果dP1P2 < dP1P3,则是第二个例子,即P2在左边。

 

现在我们有了算法的基本逻辑,我们需要知道如何计算出每个X在SX(StartX)和EX(End X)之间。因此我们首先需要计算出SX & EX。由于我们已知当前Y值以及P1P3和P1P2的斜率,我们能很容易的得到SX和EX。

 

让我们以第一个例子为例进行第一步。第一步是通过当前Y值计算出梯度(gradient)。它将告诉我们在第一步中正在P1.Y到P2.Y的哪个阶段。

Gradient = currentY - P1.Y / P2.Y - P1.Y

 

由于X和Y是线性相关的,我们可以通过梯度、P1.X、P3.X插值求出SX。 通过P1.X、P2.X插值求出EX。

 

如果你能理解这个插值的概念,你将能够理解剩下所有的教程包括处理光照和纹理。显然你必须花时间去阅读相关的代码。你还需要确定你能自己重新构建它而不是复制/粘贴代码。

 

现在我们已经描述了算法。我们开始编写代码。从删除Device类的DrawLine和DrawBLine函数开始,然后替换成如下代码:

// Project takes some 3D coordinates and transform them
// in 2D coordinates using the transformation matrixpublic Vector3 Project(Vector3 coord, Matrix transMat)
{
    // transforming the coordinates
    var point = Vector3.TransformCoordinate(coord, transMat);
    // The transformed coordinates will be based on coordinate system
    // starting on the center of the screen. But drawing on screen normally starts
    // from top left. We then need to transform them again to have x:0, y:0 on top left.
    var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f;
    var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f;
    return (new Vector3(x, y, point.Z));
}
// DrawPoint calls PutPixel but does the clipping operation beforepublic void DrawPoint(Vector2 point, Color4 color)
{
    // Clipping what's visible on screen
    if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
    {
        // Drawing a point
        PutPixel((int)point.X, (int)point.Y, color);
    }
}

 

我们已经准备好了第二个部分教程的材料。现在,是最重要的部分。基于之前的描述,这里是绘制三角形的逻辑。

// Clamping values to keep them between 0 and 1
float Clamp(float value, float min = 0, float max = 1)
{
    return Math.Max(min, Math.Min(value, max));
}
// Interpolating the value between 2 vertices 
// min is the starting point, max the ending point
// and gradient the % between the 2 points
float Interpolate(float min, float max, float gradient)
{
    return min + (max - min) * Clamp(gradient);
}
// drawing line between 2 points from left to right
// papb -> pcpd
// pa, pb, pc, pd must then be sorted before
void ProcessScanLine(int y, Vector3 pa, Vector3 pb, Vector3 pc, Vector3 pd, Color4 color)
{
    // Thanks to current Y, we can compute the gradient to compute others values like
    // the starting X (sx) and ending X (ex) to draw between
    // if pa.Y == pb.Y or pc.Y == pd.Y, gradient is forced to 1
    var gradient1 = pa.Y != pb.Y ? (y - pa.Y) / (pb.Y - pa.Y) : 1;
    var gradient2 = pc.Y != pd.Y ? (y - pc.Y) / (pd.Y - pc.Y) : 1;
            
    int sx = (int)Interpolate(pa.X, pb.X, gradient1);
    int ex = (int)Interpolate(pc.X, pd.X, gradient2);

    // drawing a line from left (sx) to right (ex) 
    for (var x = sx; x < ex; x++)
    {
        DrawPoint(new Vector2(x, y), color);
    }
}
public void DrawTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Color4 color)
{
    // Sorting the points in order to always have this order on screen p1, p2 & p3
    // with p1 always up (thus having the Y the lowest possible to be near the top screen)
    // then p2 between p1 & p3
    if (p1.Y > p2.Y)
    {
        var temp = p2;
        p2 = p1;
        p1 = temp;
    }

    if (p2.Y > p3.Y)
    {
        var temp = p2;
        p2 = p3;
        p3 = temp;
    }

    if (p1.Y > p2.Y)
    {
        var temp = p2;
        p2 = p1;
        p1 = temp;
    }

    // inverse slopes
    float dP1P2, dP1P3;

    // http://en.wikipedia.org/wiki/Slope
    // Computing inverse slopes
    if (p2.Y - p1.Y > 0)
        dP1P2 = (p2.X - p1.X) / (p2.Y - p1.Y);
    else
        dP1P2 = 0;

    if (p3.Y - p1.Y > 0)
        dP1P3 = (p3.X - p1.X) / (p3.Y - p1.Y);
    else
        dP1P3 = 0;

    // First case where triangles are like that:
    // P1
    // -
    // -- 
    // - -
    // -  -
    // -   - P2
    // -  -
    // - -
    // -
    // P3
    if (dP1P2 > dP1P3)
    {
        for (var y = (int)p1.Y; y <= (int)p3.Y; y++)
        {
            if (y < p2.Y)
            {
                ProcessScanLine(y, p1, p3, p1, p2, color);
            }
            else
            {
                ProcessScanLine(y, p1, p3, p2, p3, color);
            }
        }
    }
    // First case where triangles are like that:
    //       P1
    //        -
    //       -- 
    //      - -
    //     -  -
    // P2 -   - 
    //     -  -
    //      - -
    //        -
    //       P3
    else
    {
        for (var y = (int)p1.Y; y <= (int)p3.Y; y++)
        {
            if (y < p2.Y)
            {
                ProcessScanLine(y, p1, p2, p1, p3, color);
            }
            else
            {
                ProcessScanLine(y, p2, p3, p1, p3, color);
            }
        }
    }
}

你可以看到在代码中我们如何来处理2种三角形。

 

最后,你需要更新渲染函数来调用DrawTriangle而不是调用3次drawLine / DrawBLine 。 

var faceIndex = 0;foreach (var face in mesh.Faces)
{
    var vertexA = mesh.Vertices[face.A];
    var vertexB = mesh.Vertices[face.B];
    var vertexC = mesh.Vertices[face.C];

    var pixelA = Project(vertexA, transformMatrix);
    var pixelB = Project(vertexB, transformMatrix);
    var pixelC = Project(vertexC, transformMatrix);

    var color = 0.25f + (faceIndex % mesh.Faces.Length) * 0.75f / mesh.Faces.Length;
    DrawTriangle(pixelA, pixelB, pixelC, new Color4(color, color, color, 1));
    faceIndex++;
}

此时,你可能会感觉到你能看穿Mesh。那是因为我们绘制三角形的时候还没有隐藏三角形的背面。

 

Z-Buffering或 如何利用depth-buffer

 我们需要对当前像素在之前绘制时的Z值和当前的Z值进行比较和测试。如果当前像素的Z值小于之前的,我们覆盖并重新写入Z值。实际上这意味着我们正在绘制的面处于之前绘制面的前面。否则,如果当前像素位置的点的Z值大于之前的,我们会忽略它的绘制操作。

 

我们需要保存屏幕上每个像素点的Z值。因此,我们声明了一个float类型的数组,命名为depthBuffer。它的大小等于屏幕的像素数(width * height)。这个深度缓存在每个Clear()操作时必须初始化为一个非常大的默认z值。

 

PutPixel函数中,我们需要测试当前像素点的Z值和depthBuffer中存放的Z值。此外,之前的逻辑是返回Vector2,。我们在此将改为Vector3来存放顶点的Z值,因为我们需要这个信息来保证绘制的正确性 。

 

最后,我们通过三角形的边插值得出X值,同样的,我们需要用同样的算法插值得出Z值。

private byte[] backBuffer;
private readonly float[] depthBuffer;
private WriteableBitmap bmp;
private readonly int renderWidth;
private readonly int renderHeight;
public Device(WriteableBitmap bmp)
{
    this.bmp = bmp;
    renderWidth = bmp.PixelWidth;
    renderHeight = bmp.PixelHeight;

    // the back buffer size is equal to the number of pixels to draw
    // on screen (width*height) * 4 (R,G,B & Alpha values). 
    backBuffer = new byte[bmp.PixelWidth * bmp.PixelHeight * 4];
    depthBuffer = new float[bmp.PixelWidth * bmp.PixelHeight];
}
// This method is called to clear the back buffer with a specific color
public void Clear(byte r, byte g, byte b, byte a) {
    // Clearing Back Buffer
    for (var index = 0; index < backBuffer.Length; index += 4)
    {
        // BGRA is used by Windows instead by RGBA in HTML5
        backBuffer[index] = b;
        backBuffer[index + 1] = g;
        backBuffer[index + 2] = r;
        backBuffer[index + 3] = a;
    }

    // Clearing Depth Buffer
    for (var index = 0; index < depthBuffer.Length; index++)
    {
        depthBuffer[index] = float.MaxValue;
    }
}
// Called to put a pixel on screen at a specific X,Y coordinates
public void PutPixel(int x, int y, float z, Color4 color)
{
    // As we have a 1-D Array for our back buffer
    // we need to know the equivalent cell in 1-D based
    // on the 2D coordinates on screen
    var index = (x + y * renderWidth);
    var index4 = index * 4;

    if (depthBuffer[index] < z)
    {
        return; // Discard
    }

    depthBuffer[index] = z;

    backBuffer[index4] = (byte)(color.Blue * 255);
    backBuffer[index4 + 1] = (byte)(color.Green * 255);
    backBuffer[index4 + 2] = (byte)(color.Red * 255);
    backBuffer[index4 + 3] = (byte)(color.Alpha * 255);
}
// Project takes some 3D coordinates and transform them
// in 2D coordinates using the transformation matrix
public Vector3 Project(Vector3 coord, Matrix transMat)
{
    // transforming the coordinates
    var point = Vector3.TransformCoordinate(coord, transMat);
    // The transformed coordinates will be based on coordinate system
    // starting on the center of the screen. But drawing on screen normally starts
    // from top left. We then need to transform them again to have x:0, y:0 on top left.
    var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f;
    var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f;
    return (new Vector3(x, y, point.Z));
}
// DrawPoint calls PutPixel but does the clipping operation beforepublic void DrawPoint(Vector3 point, Color4 color)
{
    // Clipping what's visible on screen
    if (point.X >= 0 && point.Y >= 0 && point.X < bmp.PixelWidth && point.Y < bmp.PixelHeight)
    {
        // Drawing a point
        PutPixel((int)point.X, (int)point.Y, point.Z ,color);
    }
}
// drawing line between 2 points from left to right
// papb -> pcpd
// pa, pb, pc, pd must then be sorted before
void ProcessScanLine(int y, Vector3 pa, Vector3 pb, Vector3 pc, Vector3 pd, Color4 color)
{
    // Thanks to current Y, we can compute the gradient to compute others values like
    // the starting X (sx) and ending X (ex) to draw between
    // if pa.Y == pb.Y or pc.Y == pd.Y, gradient is forced to 1
    var gradient1 = pa.Y != pb.Y ? (y - pa.Y) / (pb.Y - pa.Y) : 1;
    var gradient2 = pc.Y != pd.Y ? (y - pc.Y) / (pd.Y - pc.Y) : 1;

    int sx = (int)Interpolate(pa.X, pb.X, gradient1);
    int ex = (int)Interpolate(pc.X, pd.X, gradient2);

    // starting Z & ending Z
    float z1 = Interpolate(pa.Z, pb.Z, gradient1);
    float z2 = Interpolate(pc.Z, pd.Z, gradient2);

    // drawing a line from left (sx) to right (ex) 
    for (var x = sx; x < ex; x++)
    {
        float gradient = (x - sx) / (float)(ex - sx);

        var z = Interpolate(z1, z2, gradient);
        DrawPoint(new Vector3(x, y, z), color);
    }
}



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值