实现个简单的固定渲染管线软渲染器不算复杂,差不多700行代码就可以搞定了。之所以很多人用 D3D用的很熟,写软渲染却坑坑洼洼,主要是现在大部分讲图形的书,讲到透视投影时就是分析一下透视变换矩阵如何生成,顶点如何计算就跳到其他讲模型或者光照的部分了。

因为今天基本上是直接用 D3D 或者 OGL,真正光栅化的部分不了解也不影响使用,所以大部分教材都直接跳过了一大段,摄像机坐标系如何转换?三角形如何生成?CVV边缘如何检测?四维坐标如何裁剪?边缘及步长如何计算?扫描线该如何绘制?透视纹理映射具体代码该怎么写?framebuffer zbuffer 到底该怎么用?z-test 到底是该 test z 还是 w 还是 1/z 还是 1/w ?这些都没讲。

早年培训学生时候,我花两天时间写的一个 DEMO,今天拿出来重新调整注释一下,性能和功能当然比不过高大上的软件渲染器。但一般来讲,工程类项目代码不容易阅读,太多边界情况和太多细节优化容易让初学者迷失,这个 mini3d 的项目不做任何优化,主要目的就是为了突出主干:

源代码:skywind3000/mini3d · GitHub
可执行:mini3d-src-binary

操作方式:左右键旋转,前后键前进后退,空格键切换模式,ESC退出。

特性介绍:

  • 单个文件:源代码只有一个 mini3d.c,单个文件实现所有内容,阅读容易。
  • 独立编译:没有任何第三方库依赖,没有复杂的工程目录。
  • 模型标准:标准 D3D 坐标模型,左手系 + WORLD/VIEW/PROJECTION 三矩阵
  • 实现裁剪:简单 CVV 裁剪
  • 纹理支持:最大支持 1024 x 1024 的纹理
  • 深度缓存:使用深度缓存判断图像前后
  • 边缘计算:精确的多边形边缘覆盖计算
  • 透视贴图:透视纹理映射以及透视色彩填充
  • 实现精简:渲染部分只有 700行, 模块清晰,主干突出。
  • 详细注释:主要代码详细注释

截图效果

颜色填充

image

透视纹理映射

image

线框图

image

增加光照和二次线性插值(朋友给 Mini3D加的平行光源)效果还行:

image

阅读要求:

  • 看过并了解 D3D / OGL的矩阵变换。
  • 用 D3D / OGL 完成过简单程序。

实现说明:

  • transform:实现坐标变换,和书本手册同
  • vertex: 如何定义顶点?如何定义边?如何定义扫描线?如何定义渲染主体(trapezoid)?
  • device: 设备,如何 projection,如何裁剪和归一化,如何切分三角形,如何顶点排序?
  • trapezoid:如何生成 trape,如何生成边,如何计算步长,如何计算扫描线
  • scanline:如何绘制扫描线,如何透视纠正,如何使用深度缓存,如何绘制

基础练习:先前给学生的作业

  • 增加背面剔
  • 增加简单光照
  • 提供更多渲染模式
  • 实现二次线性差值的纹理读取

扩展练习:给有余力的学生

  • 推导并证明程序中用到的所有几何知识
  • 优化顶点计算性能
  • 优化 draw_scanline 性能
  • 从 BMP/TGA 文件加载纹理
  • 载入 BSP 场景并实现漫游

其他内容

当年还用不了 D3D 和 OGL ,开发游戏,做图形实现软件渲染是必备技能,当年机型差,连浮点数都用不了,要用定点数来计算,矩阵稍不注意就越界了。计算透视纠正还是一个比较昂贵的工作,更多游戏使用仿射纹理绘制,只是把离屏幕近的多边形切割成更小的三角形,让人看起来没有那么明显。即便到了 Quake 年代,计算 1/z 的除法也只是四个点才算一次(经过精确计算CPU周期,绘制四个点时下一个点的 1/z刚好算完),Quake 的四个点内也还是仿射纹理绘制……

那时显卡没普及,光软件渲染器的优化就是一个无底洞,今天有了 OGL/D3D和显卡,人的精力才能充分集中在更高层次的场景组织、层次细节、动态光照等功能上。然而有空的时候,花个一周时间坐下来了解一下这部分的大概原理,推导所用到的数学模型,也能帮助大家更好的理解底层运行机制,写出更加优化的代码来。

PS:光线跟踪版本的软件渲染,考虑光照的话,简单实现起来差不多 500 行,比这个要简单一些。各位有兴趣也可以尝试一下,就是简单渲染个立方体足够了。