软渲染示例程序Tiny3D的实现简介

Table of Contents

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

绘制图形的代码

Tiny3D核心的图形绘制代码调用栈如下图所示

核心图形绘制代码调用栈

Device::DrawBox函数

Tiny3D的核心绘制代码的入口函数Device::DrawBox函数

// theta mesh的旋转角度
// box_vertices mesh的原始模型顶点数据
void Device::DrawBox(float theta, const T3DVertex* box_vertices)

Device::DrawBox函数的实现如下代码所示:

void Device::DrawBox(float theta, const T3DVertex* box_vertices)
{
    T3DMatrix4X4 m;
    T3DMatrixMakeRotation(&m, -1.0f, -0.5f, 1.0f, theta);
    transform_.SetWorldMatrix(m);
    transform_.Update();
    DrawPlane(&box_vertices[0], &box_vertices[1], &box_vertices[2], &box_vertices[3]);
    DrawPlane(&box_vertices[4], &box_vertices[5], &box_vertices[6], &box_vertices[7]);
    DrawPlane(&box_vertices[0], &box_vertices[4], &box_vertices[5], &box_vertices[1]);
    DrawPlane(&box_vertices[1], &box_vertices[5], &box_vertices[6], &box_vertices[2]);
    DrawPlane(&box_vertices[2], &box_vertices[6], &box_vertices[7], &box_vertices[3]);
    DrawPlane(&box_vertices[3], &box_vertices[7], &box_vertices[4], &box_vertices[0]);
}

步骤就是如下的几步:

  1. 根据传递进来的mesh的旋转角度theta,调用T3DMatrixMakeRotation函数构建world matrix,然后更新到Transform类中去
  2. 调用Transform::Update函数,更新world-view-projection matrix,这WVP matrix将在后面的用来变换顶点。
  3. 把一个六面体BOX拆分成6个矩形面,调用DrawPlane函数进行绘制。

Device::DrawPlane函数

Device::DrawPlane函数的实现如下代码所示:

void Device::DrawPlane(const T3DVertex* p1, const T3DVertex* p2, const T3DVertex* p3, const T3DVertex* p4)
{
    T3DVertex _p1 = *p1, _p2 = *p2, _p3 = *p3, _p4 = *p4;
    _p1.tc.u = 0.0f, _p1.tc.v = 0.0f;
    _p2.tc.u = 0.0f, _p2.tc.v = 1.0f;
    _p3.tc.u = 1.0f, _p3.tc.v = 1.0f;
    _p4.tc.u = 1.0f, _p4.tc.v = 0.0f;
    DrawPrimitive(&_p1, &_p2, &_p3);
    DrawPrimitive(&_p3, &_p4, &_p1);
}

Device::DrawPlane函数中,就是把四边形的四个顶点,分别组成两个三角形,然后调用Device::DrawPrimitive函数绘制这两个三角形。

Device::DrawPrimitive函数

此函数比较长,就不在这里摘录了,该函数的签名如下:

void Device::DrawPrimitive(const T3DVertex* v1, const T3DVertex* v2, const T3DVertex* v3)

该函数的内部实现步骤如下:

  1. 使用Transform::Apply函数,对组成三角形的三个顶点v1,v2,v3,进行空间变换,将这几个顶点变换到裁剪空间。如下面的“裁剪空间和NDC坐标系”图所示。
  2. 使用Transform::CheckCVV函数,判断这三个顶点是否有超出裁剪空间范围内,目前的算法是:这三个顶点只要有一个超出范围,即放弃渲染此三角形。
  3. 完成了裁剪操作之后,使用Transform::Homogenize函数执行透视除法,将顶点变换从clip space变换到NDC坐标系以及屏幕坐标系中去。
  4. 如果是纹理映射模式RENDER_STATE_TEXTURE)或者颜色填充模式RENDER_STATE_COLOR),使用Trapezoid::SplitTriangleIntoTrapezoids函数,将三角形分割成两个梯形,然后调用Device::RenderTrapezoid函数进行绘制。
  5. 如果是线框模式RENDER_STATE_WIREFRAME),即调用Device::DrawLine函数直接绘制三角形的三条边。

裁剪空间和NDC坐标系






把三角形拆分梯形并渲染的Trapezoid::SplitTriangleIntoTrapezoids函数



Trapezoid::SplitTriangleIntoTrapezoids函数

Trapezoid::SplitTriangleIntoTrapezoids函数把一个三角形拆分成两个梯形。这个函数代码较长很长,在此不做转录。具体步骤也在代码中已经注释清楚,这里贴一个辅助阅读代码的图

把三角形分割成梯形并渲染的示意图



Device::RenderTrapezoid函数

Device::RenderTrapezoid函数就是绘制梯形的函数,截下上面的“把三角形分割成梯形并渲染的示意图”来看。可分为以下步骤:

  1. 根据传递进来的梯形对象指针trap,算出该梯形的顶边和底边分别对应于屏幕中的哪几行。假设从top行开始到bottom行结束
  2. 在top行至bottom行范围内循环,在当前的第y行屏幕行中,trap调用其成员函数Trapezoid::CalculateEdgeInterpolatedPoint。处理梯形在当前行中左右腰边的插值点,这插值点用来作为扫描线的端点
  3. 在top行至bottom行范围内循环,在当前的第y行屏幕行中,trap调用其成员函数Trapezoid::InitializeScanline。处理梯形在当前行中,对应生成的逐行扫描的扫描线。
  4. 在top行至bottom行范围内循环,在当前的第y行屏幕行中,调用Device::DrawScanline函数,绘制扫描线

参考文章

Homogeneous Coordinates, Clip Space, and NDC | WebGPU

clip space and NDC space are not the same thing

kumakoko avatar
kumakoko
pure coder
comments powered by Disqus