Home > 图形编程, 编程技术 > 如何写一个软件渲染器?

如何写一个软件渲染器?

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

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

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

源代码:skywind3000/mini3d · GitHub
可执行:http://www.skywind.me/mw/images/c/c8/Mini3d.7z

操作方式:左右键旋转,前后键前进后退,空格键切换模式,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 行,比这个要简单一些。各位有兴趣也可以尝试一下,就是简单渲染个立方体足够了。

Categories: 图形编程, 编程技术 Tags:
  1. chris
    August 25th, 2015 at 12:43 | #1

    帅哥,你这个真的是业界良心。不过可以不可以写篇文章配合代码讲解讲解原理性的东西腻

  2. August 31st, 2015 at 08:59 | #2

    @chris
    看懂这个之前先要熟悉 D3D 的基本原理呀,我这个注释已经满详细的了嘛,如果需要,等我有空再整理下吧。

  3. IceCoffee
    September 2nd, 2015 at 09:32 | #3

    很好的教程,希望楼主有空再做个详细点的解释哦,照顾一下没看过矩阵变换的同学:-)

  4. nobody
    October 28th, 2015 at 18:43 | #4

    IceCoffee :很好的教程,希望楼主有空再做个详细点的解释哦,照顾一下没看过矩阵变换的同学:-)

    嗯,D3D有大量教程,进行这个课程前,起码要用D3D写过旋转立方体这样简单的DEMO,并且弄清楚背后的数学啊。

  5. chris
    December 3rd, 2015 at 11:09 | #5

    @IceCoffee
    我把d3d涉及 的矩阵知识学了一遍,知道了相机具体存在的形式,旋转,平移,缩放,投影。
    现停在 扫描线和帧缓存这里 ,没有完全理解扫描线是什么

  6. chris
    December 3rd, 2015 at 11:12 | #6

    帅哥有时间 搞一个稍微详细的图,描述下渲染的执行过程o(∩_∩)o

  7. Shihira Fung
    February 17th, 2016 at 22:38 | #7

    你好。

    有点问题想请教一下。深度测试那里,为什么是用rhw来做,而不是z?

  8. February 18th, 2016 at 19:20 | #8

    @Shihira Fung
    实际是w缓存,经过projection矩阵乘法后,w和z是成线性关系的(具体见透视投影矩阵生成),固定管线标准模型中,都是用w进行缓存的,而rhw是保存着1/w的值,在具体绘制的时候才会计算 w = 1/rhw,除法代价大,因此直接用1/w进行判断,越远的点rhw值越小,越近的点,rhw值越大,所以深度缓存中rhw越大的值覆盖越小的值,因为他们代表越近的像素点。这样清空深度缓存时填写0就行了。不需要笨拙的用传统的z判断,初始化成一个很大的值,每次判断更小的值被保留下来。

  9. Christal
    April 8th, 2016 at 23:17 | #9

    博主,请教一个问题。
    假如我要给Mini3D添加光照,而且不是Gourand是Phong,也就是要给每个pixel计算法向量。因为每个pixel是在线性插值的时候计算出来的,这个时候pixel的坐标x和y分量是在屏幕空间中的,但是Phong光照模型计算需要计算入射向量和观察点到该点的向量,光源坐标和光差点坐标是在世界空间的,这样来怎么计算入射向量和观察点到该点的向量呢

  10. Christal
    April 9th, 2016 at 01:03 | #10

    请教博主一个问题:
    假如我要给Mini3D添加Phong光照。Phong光照需要计算每一个pixel的入射光向量和观察点到pixel的向量,但是由于在求pixel的时候已经是在屏幕空间中进行线性插值了,这个时候求出来的x和y分量不能用来求入射光向量和观察点到该点的向量,该怎么办呢?

  11. aceyan
    June 13th, 2016 at 15:39 | #11

    @Christal
    你想做phong像素光照的话实际上是这样一个过程:
    顶点程序中保存转换到世界空间的法线(如何你打算在世界空间计算光照的话),保存世界空间顶点位置,传递到片段程序。
    片段程序中利用顶点传递的法线和顶点位置进行光照,这样光照还是在世界空间进行的呀,放到顶点里面计算是为了让法线得到透视校正插值,这样的光照效果更加精确。

  12. August 18th, 2016 at 17:29 | #12

    @Christal
    很简单,你需要将三维空间的 x,y,z 一起放入插值,即(一开始/w),进行屏幕空间插值,插值完后再* w。这个问题我新文章里面有解释:http://www.skywind.me/blog/archives/1828 类似 OpenGL的 varying 的标准插值方法,希望能帮助到你,给 mini3d 添加 varying 很简单。

  13. Shady
    July 27th, 2017 at 10:14 | #13

    博主,请教一个问题。
    我看在扫描线绘制的时候,顶点属性进行了透视矫正插值(z(b1/z1 + t(b2/z2 – b1/z1))),但是在生成水平扫描线时,扫描线的左右两端点属性是直接线性插值的结果(b1 + (1 – t)b2)),没有进行透视矫正。不知道是不是我的理解有问题,希望博主指点一下。

  14. demon
    July 27th, 2017 at 10:23 | #14

    博主,请教一个问题。
    我看在扫描线绘制的时候,顶点属性进行了透视矫正插值(z(b1/z1 + t(b2/z2 – b1/z1))),但是在生成水平扫描线时,扫描线的左右两端点属性是直接线性插值的结果(b1 + (1 – t)b2)),没有进行透视矫正。不知道是不是我的理解有问题,希望博主指点一下。

  15. Shady
    July 27th, 2017 at 10:25 | #15

    我看在扫描线绘制的时候,顶点属性进行了透视矫正插值(z(b1/z1 + t(b2/z2 – b1/z1))),但是在生成水平扫描线时,扫描线的左右两端点属性是直接线性插值的结果(b1 + (1 – t)b2)),没有进行透视矫正。不知道是不是我的理解有问题,希望博主指点一下。

  1. August 28th, 2021 at 00:15 | #1

Wordpress Social Share Plugin powered by Ultimatelysocial