经常有人说浪漫这个词。在实用主义的人们看来,浪漫基本跟没用是一个意思。在计算机的世界里,同样存在很多看似“浪漫”的事物,比如标题中的软件渲染器。
什么是软件渲染器?
图形处理器
我们经常提到 GPU (显卡),性能优良的 GPU 是流畅运行游戏的基础。GPU 和 CPU 常被拿来比较。如果说 CPU 是为通用计算而设计,那么 GPU 就是专为游戏而生。CPU 只有几个内核,而 GPU 有数百或数千个内核,经过优化可以并行运行大量计算。正因如此,虽然 GPU 为游戏而生,但它对运行分析深度学习和机器学习算法尤其有用 (比如说现在都有了个 OpenCL) 。
渲染
所谓渲染,就是用软件从模型生成图像的过程。模型是用数据结构进行严格定义的三维物体或虚拟场景的描述,它包括几何位置、纹理坐标、光照等信息。图像是数字图像或者位图图像。
渲染一般分为预渲染和实时渲染,都非常慢。预渲染用于电影制作,计算强度大,需要大量的服务器运算完成。实时渲染则常用于 3D 游戏,通常由 GPU 完成计算过程。
渲染管线
渲染管线是一条流水线,它里面流淌的是用于渲染的数据。对于 3D 渲染来说,一个图元 (通常是三角形) 需要经过渲染管线各个阶段才能被显示在屏幕上。
硬件加速
在 GPU 面世之前,我们用 CPU 来进行绘图显示的。CPU 计算能力強,但并不能并行处理海量数据。而 GPU 是单一指令多数据流 (SIMD) 架构,能在单一时钟循环下,多条管线平行处理指令。直接在 GPU 中完成渲染管线的许多阶段 (比如顶点变换和光照) ,能够达到使用硬件加速渲染的效果。另外,原本 GPU 的硬件加速是为了高性能 3D 图像而设。到后来,也将对影像和 2D GUI 的加速功能集成在了一起。
OpenGL 和 DirectX
随着 GPU 的蓬勃发展,OpenGL 出现了。OpenGL 是一个用于渲染的跨平台库,向下直接跟 GPU 硬件驱动打交道,抽象了渲染管线的各个阶段,以 API 的形式提供出来。而 DirectX 则是微软版本的 OpenGL,游戏配置需求表就经常出现 “DirectX: Version 11” 。
软件渲染器
现在的渲染过程已经从固定管线变成了可变管线,GPU 支持的高级功能也越来越多,但这并不防碍我们去理解每一个像素是如何被渲染到屏幕上的。OpenGL 虽然大部分是为了使用硬件加速而设,但是理论上完全可以通过软件的形式实现。用软件程序的形式来模拟实现图形渲染管线,这就是软件渲染器。在没有 GPU 或者 GPU 不能用的情况下,OpenGL / DirectX 会使用内置的 “高速” 软件渲染器来完成渲染过程。
所以为什么说软件渲染器是浪漫 (没用),一是因为大家一般都使用硬件加速,没有用武之地;二是最好的实现在 OpenGL / DirectX 里,写软件渲染器并不是一种 “创造”。
写软件渲染器所需要的知识
一门语言 + 3D 光栅化渲染算法 + 输出环境
语言没有啥特别的要求,反正我看到过 c,c++,c#,python,java,javascript… 输出环境的最低要求是能提供一张 bitmap 来操纵和显示,程序往 bitmap 的每个单元写颜色值,输出环境把这张 bitmap 展示到屏幕上。我用的是 c++ 和 windows api / gdi (win32 application),c# 可以用 winform (更省一点直接用 unity), js 可以用 canvas…
然后是光栅化的渲染算法。这一套算法看 3d graphics pipeline 就可以了解大概有什么:基本上就是 顶点变换
和 光栅化
。顶点变换很多书都会比较仔细提到,光栅化算法则少见得多。
为什么要写软件渲染器?
写软渲之前,学习 DirectX 的时候,真的是碰到很多术语。尽管网络上,书上都有相当程度的解释,但总是感到不尽然。非得概括这种感觉的话,应该就是“没亲自写过的就不是自己的”吧。但直接使用 DirectX 就已经相当底层,而软渲基本是实现个阉割版 DirectX,就更底层了。花时间去学习理解这些基础的算法和流程有没有必要?我不能下这个结论。所以只能把我认为写软渲所能得到的写下来:
- 细致地理解光栅化渲染的流程以及所用到的经典算法、数据结构和经验计算模型
- 使相关知识牢靠和启发图形化的调试技巧
- 避免手生和打发时间
挨个解释下:
1. 很好解释。如果这些东西不熟悉,软渲是写不出来的。
2. 渲染器的输出是一张图像,这就意味着调试不是看文本化的输出,而是看图形化的输出。渲染管线涉及的步骤并不少,其中任何一个都有可能导致最终的渲染错误。这就需要了解一个步骤的细节,并且当渲染出魔性的图像时,按照它去推断可能出错的地方。
3. 通用的道理。动手实践对于技术人员而言,是深入骨髓的东西,需要不断地练习巩固。
我的软件渲染器
感想
这次写软渲,除了光栅化算法本身,我印象最深的就是总不离不弃的 bug 们。从 void** 转成二维数组错误导致 violent access,最后使用指针数组解决;扫描线填充时因为顶点顺序错误,导致颜色插值错误;三角形编织顺序不对,导致背面剔除错误; float 相等判断的方式引起的深度缓冲和单元测试错误…但正是这些 bug 才是独特的经验。
还有就是渲染器的组织方式,我还想过挺多。分离渲染算法和渲染数据,渲染物体改成渲染场景中的物体,数学库是短小精悍还是大而全,const buffer 放在 scene 里…虽然很多时候都被证明是没必要的想多了,但是多思考总应该不会有什么危害吧…
本来我还想写多点的,但是恰巧读到一个故事:sun 公司的最后一任 CEO 热衷于写博客,然后 sun 公司就完蛋了,堪称博客误国的典范…他被批评说 “无论再好、再多的博客都代替不了好的处理器、软件和销售”。虽然只是调侃吧,但老让我心有余悸…所以以后我写博客还是简洁为上。
另外,果然干什么都有一个亘古不变的硬伤:想的很多,干成的很少。我这个软件渲染器只实现了最基本的功能,其他的包括外部加载数据、各种贴图支持、Shader 结构等都没有加上去,是真的懒…
bug组图
这些 bug 图并不是一开始就有意记录的,所以前期一些非常魔性的 bug 并没有截图下来(实在遗憾)。有时候, bug 也可以很美丽。
图片(组) 2.我也记不清哪里出现的各种 bugs
项目地址
Software Renderer – Github
原文链接