|
(第一不分先讲背景:技术一点的内容在下面)
大学时,有一段时间对渲染非常感兴趣。当时在开发一个类似谷歌地球的东西,有好多好玩的小功能。主要是用3D自由摄像头和时间线来控制探索历史变迁什么的。虽然跟专业的关系不大,但是正好可以结合一些数学+地图+历史+编程的爱好。所以投资了好多时间。
时间久了,很多东西都忘掉了。只记得那些大致的小发现或小方法。(有一段时间在谷歌搞WebGL相关的,街头实拍什么的,但是那个项目不怎么需要动脑子)
前两天,突然发现知乎上有一大堆渲染类话题。一下子,各种回忆都突然想起来了。然后很想写这方面的回答。花了整个周末把之前的代码找出来,结果已经找不到了。(硬盘太多,很多都已经挂了不能再用)。
所以今天给自己休假一天,尽量把一些当时的原理找回来重新实现。
同时,希望对大家有所帮助。
(为何跟话题有关,最后再写个总结针对这个问题,可能更清楚一些)
---
首先,我的渲染逻辑和思路跟大家都不太一样。差距较大。
1)我比较喜欢从设备硬件基础功能(底层输入)出发。因为我喜欢控制里面每一个细节。随我来说GPU的shader language(渲染语)就是万能的。
2)现在的主流行业思维是"bottom-up"。调用各种成熟的高级功能;把3d世界里所有对象都渲染出来,再想办法简化速度化。最后变成一张渲染结果。我比较喜欢“top-down”,这张图上每一个像素最后到底该渲染什么颜色,让每一个像素去探索世界好了。
3)什么三角形组成的polyhedra、polygon之类的,我一点都不感兴趣。虽然也比较熟悉,但是从一开始就有极大发自内心排斥感。一个像素并不需要知道世界上所有三角形在哪儿。
4)时间time(比如动画)也只是多了一个数据维度,并不需要每次重新画。跟换一个3D角度一样,甚至更容易。(虽然这一篇不太涉及得到,以后再写)。
5)不太在乎亮光,影子效果这种玩意。或者说我的重点放在渲染能力及效率,而不在美感方面。
(对颜色什么的也不是特别敏感,或者感觉次要)
有点类似raycasting,但是要考虑到最现实的那些问题。如何去画任意复杂形状的东西?我并不想画什么简单玻璃球形之类的呢?
---
OK, 为了这个演示,准备了一个python小库lib.py:
也就这么一点。不详细解释了。反正就是简单利用一个shader画(Draw)一张图得了。
Python可以简略一些,这方面。每次调用Draw,假设所有像素(屏幕)在 [-1,-1]:[1,1]之间一个正方形空间。
之后的工作,那就是Fragment Shader来负责。重点放在Fragment Shader。每个像素被并行处理。Vertex Shader(+Geometry Shader)那一套暂时不管了,我只想要一个每个像素独立并行计算颜色的空间。
如果读者不熟悉:我比较喜欢的这个 Fragment Shader 所负责的就是:一个像素到底显示什么颜色。唯一的输入就是它在屏幕上的位置。GPU能够将所有像素同时一起算出来,所以这种事情不太适合CPU。当然了,处理一个像素的计算能力相当弱,要慎重。照样可以干很多事情了。
运行脚本是这样的:
尽量用最简单的,这个while loop可以一秒画一百张,也可以画一张直接保存图片再退出。这一些不是很重要。(这里的Draw是以2.5的距离慢慢随机旋绕 [0,0], 重复每秒渲染100次左右)
Uniform(不变量)是什么?是发给所有像素Fragment Shader的共同变量。不变是因为任何像素都看到一样的值。这样做会方便一些。比如摄像头位置(rayOrigin),摄像角度之类的。要不然所有像素到底怎么知道渲染什么?比较重要。随便提供什么Uniform都可以输入给Fragment Shader。
我的Fragment Shader一般怎么写?这里得先解释思路。
-----
比较喜欢“立方体”cube。因为一切计算都更简单。其实整个思路建立在立方体上面。
我们第一个目标是渲染一个立方体:
Simple Fragment Shader:
上面加载所有uniform。
中间是一个函数。从一个像素[-1.1]^2的位置,算出它在3D世界里的一条线rayDirection。
这一段就跟raycasting/raytracing一样。
(camera origin就是我的那个rayOrigin)
我们这个像素的一条线发出来了,碰到什么?这不就解决了吗?
碰到了以后,通过distanceTravelled就可以获得与对象相交的位置。
“gl_FragColor”是Fragment Shader主要固定输出;也就是这个像素的最后颜色。
(这里,为了简化,就直接把对象相相交点的位置转换成颜色。不太擅长这个。计算机颜色是三四个0-1的权重,一个四维向量,然而对象位置也是一个-1到1的向量。比起同一颜色好一些。如果这条线没碰到任何对象,那就涂为黑色:0)
探测一个立方体是这样:
原理很简单:
解释:任意一条线必须跟正方形的四个边相交。(除非parallel,现实中可忽略)
然后相交的次数决定这条线是否进入了正方形。
(把这个概念放到三位、四位、还是一样)
所以有这样的一张图:
(把这些代码加起来)
-----
好了,那下一步;(实际上一直在旋绕,只要截图)
如何显示任意形状?
这里就得介绍一个cubetree(立方体树)的概念:
每一个立方体都可以分为八个小立方体。其中每一个可以再次切小。切小切削到比一个像素还要小,根本看不出来是立方体所组成的。
代码:
我知道有点复杂。可是我用了类似的算法渲染过各种东西。已经差不多最简化了。
意思是这样;一旦发现了一个立方体,再调查该立方体里面的八个部分(树根)。
一条线一直从摄像头往前走,经过大的小的立方体。什么时候遇到空的立方体,就可以直接放弃了走到下一个了。大概这样:
只不过是把这个思维做成三维以上。
走到了某个立方体,对方可以宣布自己是空的,然后直接掠过,往前跳过。也可以宣布自己里面有东西,拆开走更小步。结果真的没碰到东西,那就可以去下一个立方体了。
比如这样:
每一个立方体都报告自己是否与一个对象重叠。(比如一个球表)
(分别是4层,8层,12层)
(13层以上没什么用,因为已经比一个像素还小;毕竟是倍数详细化的)
这样算,也挺快的,比一百万个三角形都快。因为已经是O(logN)。
每个像素都能快速扎到对象边界,有点类似binary search(把这个方法做到一维空间也就是binary search好吗?)只不过是每一个像素独立进行自己的search。
当然不仅限于球形什么的。还可以快速渲染fractal。
(这里是个最简单的sponge fractal;其他fractal都一样快。还不需要Distance Estimation微积分什么的。可以直接让立方体自己测试)
代码:
(唯一的区别就是大小cube是否存在-是否与fractal重叠-的那一段代码)
这还不是特别优化的代码,怎么这么快?这个ray还能选择性测试前面的大小立方体。所以根本不需要1024*1024*1024这么多小立方体。因为树根结构,每一条线(每个像素)只需要测试自己经过的30-120个。当然有用!
(唯一的区别就是cube是否存在的那个函数代码)
用这种方法渲染fractal也很好玩。以后再专门写一篇。
(现实这种复杂一点的fractal也可以,但是要hack一下代码。当然不是这个小程序做的图,但是差不多,只不过颜色没那么精致。颜色反射这方面我没有研究很多。)
-----
这大概是几个基本的概念。一两天也只能做到这里。
当然可以用这种方法来渲染更复杂的model。以前开发的东西就是这样的。
把polygon model转换成立方体树根就比较难了。这里还没来得及重新实现。甚至没介绍4D texture的用途,如何把树根写成texture,等等。反正,转换了之后,一切都很简单。整个scene或animation都可以用最简略(压缩)的同一个树根来描述。
为何比普通渲染流程强?因为无论场景有多复杂(无论多少个对象),时间需求都差不多。近的远的若干个对象的形状,都是O(logN)的效率来渲染。彻底利用了GPU那种并行计算的能力。多余的计算也没多少。
当然还有很多问题存在;可是一切普通fragment shader能干的(什么颜色影子之类的),一样能补充进去。关键是把一个立体空间简化成了一个数根结构,更适合渲染。
到底有多少公司或产品在用这种方式?并不是很多。这种软件做法,减少渲染效率,如果加油开发;能够改变好多。有点类似binary search/sort方法对于快速search的贡献。
软件方面还可以提高那么多,感觉很明显了。
(单独用fragment shader来做渲染,也不是很理想。主要因为目前的硬件支持。因为这种树根需要搜索texture。GPU的设计不太包容这种几百次反复搜索texture和pointer的做法。一般GPU最多一百次调用texture。有些workaround/loophole可利用,以后再讲,可是整体GPU结构不够支持这种算法。我一样感觉这就是未来的渲染路线之一。不管是软件还是硬件,这个算法不可缺少)
(好多细节没来得及解释了,道歉,今天写的这一篇有点急。 看一看代码的comments吧?) |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|