2002-12-2
好像很久没有写Looking日记了,好像有一个月了吧。过去的一个月里发生的事情,我想我一生都不会忘记。我的一个朋友惹上的刑事官司,在他的父母再三恳求之下,我居然成了他的代理人。从那天开始,我的生活就进入了社会的令一个层面。我以前是一个不太接触社会的人,我周围的人基本上都是大学毕业,有1/4的人是硕士及以上学历,我一直认为这个社会是文明、礼貌的社会。但,当我来到公检法这个圈子里后,我发现我好像到了火星。当我第一次来到看守所为朋友办手续时,接待我的是个老公安。几句话后,他发现我没有一点经验后,劈头盖脸的就是一句:你是不是不接触社会,是不是念书念傻了。当时,我一脸窘迫,点点头说,确实是念傻了。后来,可能是觉得我确实傻的可爱,老公安开始为我指点了一些门道。就这样,我踏上了一段奇怪的旅程。后来,我每天往返于刑警队和检查院才知道,那天的遭遇,真是小菜一碟。不过,司法部门的工作人员的态度虽然差了一些,但还是比较公正的,检察院驳回了对我的朋友的指控。在这20多天里,我还是有很多收获的,至少我的法律意识提高了很多,我感觉以前的我简直就是个法盲。
还是继续说我的Looking吧。自从进而了D3D的世界,我就处于发现和回忆状态。以前虽然写过一些OPENGL的代码,但那毕竟是两年前的事情了,重新建立我的3D编程概念是当前最重要的。总的感觉D3D在功能和概念上与OPENGL是非常类似的,在程序界面和调试功能上好像要比OPENGL优胜一些。由于我看D3D的文档比较吃力,于是我在网上寻找了很多资料,不过有价值的很少,特别是中文资料。有一个很有趣的现象,国内的技术网站的内容重复率真是惊人的高。我在Google上查找资料的时候,最害怕的就是某个生僻的单词被某篇中文资料收集,如果发生这种情况,就不用干别的了,先翻20页再说。绕了一大圈,最后还是苦读M$文档,说来也怪,这次看M$文档就亲切多了,可能是没有后路了的原因吧。
到目前为止,我对3D世界里的空间管理的兴趣要比渲染的兴趣大的多。因为,我认为空间管理才是3D引擎的核心和关键任务。在3D引擎里,像ZBuffer这样普通、通用的技术都是多余的,越高级的功能速度越慢。因此,我没有太多地把注意力放在光照和材质这类事情上,我只是在,Camera的位置上放了一个同向的Spotlight,它的范围和camera space相当,对于我来说,只要能看清楚就行。我首选要解决的问题是camera位置的调整和world space的旋转,我必须能够看到3d space里的各个细节才能进行其它工作。在写Looking编辑器的时候,我的首选参照对象是3DMax,因此每增加什么功能我都会去先研究研究3DMax。在研究3DMax的world space旋转功能时,我发现了我以前在概念上的一个误区。以前在写Looking的时候,显示实际效果的view是D3D view(使用D3D技术渲染的view,呵呵纯粹的俚语),而3个投影view是普通的windows窗体。但3DMax显然不是这样处理的,他的4个view都是得D3D view。这样作有很多显而易见的好处,其中最重要的是在编程界面上所有的view得到了统一。这个技术的关键在于效果view使用的是普通的projection,而投影view使用的是orthogonal projection,也就是投影view的view平截头是方的。这样在效果view里的操作和投影view里的操作就几乎是一样的了,唯一的不同可能只是FVF的差异和投影view里不需要光照并且投影view只需要画出framework。这也意味着,我以前作的很多工作完蛋了。
我一直认为程序员在编写程序的时候,最应该花费精力的是概念和结构,至少不是编写代码。这就像开车一样,如果你的注意力始终在挂档上,那你肯定是个新手。教我开车的师傅说过的一句话令我印象非常深刻:如果你在时速80脉的情况下,还能知道路边的女孩是否漂亮,你就是个成手的。由于我的工作关系,我经常要培训一些新兵。我经常会问他们,在编程的时候你在想什么?这不是在责问他们,而是我确实想知道他们在想什么。在我看来,编程时在想什么是评价一个程序员技术能力的最基本的指标之一。在编写一个项目的过程,有时就是寻找某个概念的过程。当项目完成后,这个概念也就丰满了,项目顺应这概念的曲线而存在。一个项目是什么样子的?这就是项目的样子。如果,一个项目完成了,还总结不出点东西,那这个项目肯定是失败的项目。
解决了view的问题后,我需要作一些辅助性的基础工作。由于我是D3D的新手,我必须要编写大量的试验性代码。但是,D3D的FVF令我非常厌烦。每个函数都要增加新的struct,尽管我把这些struct定义在函数体内,并使用相同的名字,但还是太多了。这时我需要一个通用的D3D渲染函数,我不需要追求速度,只要通用就行。要实现它,我必须提出一个数据源的概念,我把它称为Solid。有了Solid,就需要一个Face的概念;有了Face,就需要一个Polygon的概念;有的Polygon,就需要一个Vertex Pool的概念。这时候,我想我已经越界了。这些概念都是3D Engine的概念,而不是编辑器的概念。如果任由其发展下去,以后会产生概念冲突。于是我建立了一个新的DLL工程,叫LJet。它将是Looking编辑器、Looking编译器和Looking解释器的通用API。写到这里,我开始越来越喜欢D3D了。D3D不禁提供了丰富的渲染功能,而且它还提供的丰富的3D数学支持。像Vector、Matrix、Plane、Quaternion支持函数,应有尽有。如果使用OPENGL这些恐怕都要自己写,虽然难度不大,但代码量却不少,而且还需要大量的调试。现在就简单多了,我可以直接进入主题。当然也不能原样照抄,否则离开了D3D,LJet就玩不转了。在LJet中我使用J作为系统前缀,这样在所有的D3D结构里我把D3D或D3DX字样替换成J后typedef一下,在相关的函数里,我把D3D或D3DX字样替换成j后#define一下,以后要摆脱D3D恐怕就得#if #else #endif了。
现在,我要重新考虑component的问题了。既然定义了通用的data source格式,组件的接口需要进行一些调整。由于组件本身其实就是一个Solid,因此它只需导出Solid实例就可以了。好在我现在只有一个component,一个cube component。我认为,在研究3d engine时,一个cube就足够了。它足可以组成相当复杂的场景。但是,可能是因为好玩,我一口气又作了球体、圆柱、台锥、圆环、棱锥、管子和大茶壶等7个component。只要是3DMax的标准组件,我都完成了一个,就像下面那个图片所显示的,很有趣吧。当然这些组件还有不少瑕疵,不过以后在修改吧。
越研究3DMax越觉得它是个好东西。该软件在交互上的创意真是神来之笔,我真想把这些都搬到我的系统里。这时候,Looking编辑器又来到了一个十字路口。Looking编辑器现在越来越具备了商业级3D图形编辑软件的特征。这时候,我很矛盾,是在编辑系统里继续发展下去,还是向3d engine转移注意力?最后,我选择了后者,毕竟这是我长久以来的一个愿望。但是压制继续发展Looking编辑器的强烈愿望,令我很遗憾,我想我以后会再去发展它的。
再次开发Looking已经有一个半月了。在此期间,我一直在躲避某个东西,我深深的对它有敬畏感。其实它就是LJet。再此之前,我小范围的发展了一下LJet,没有敢轻举妄动。原因有两个,一方面Looking编辑器还没有足够的功能来支持LJet的调试;另一方面,我感觉我的状态还不够好,不足以完成它。在长年累月的编程工作中,状态的起伏是不可避免的。在状态不会的时候去编写核心代码,会产生大量的废码,甚至导致在思维上的崩溃从而放弃项目。但是,我们又不能不工作,因此如何调整状态在程序生涯中是非常重要的。如何在状态不好的时候完成机械性、辅助性工作,如何在状态好的时候完成核心工作?我给这种调整起了个名字,我套用一个围棋术语“腾挪”,或者叫“洗衣机”,呵呵下围棋的朋友一定知道什么意思。但遗憾的是国内公司的管理实在是成问题,至少我呆过的公司都这样。当我腾挪的时候,往往会同领导产生分歧。解释一般是徒劳的。领导一般认为程序设计是可以定量、定时的。这真是很可笑,也很可叹。我曾经给某个领导说了个比喻,我问他:这有一堆砖头,你让我把它砌成一面墙;你可以很清楚的定量、定时,我可以作,但我不能保证它不坍塌;你认为我的工作性质和砌墙类似么?我不知道他听懂了没有?往往,为了保质量的完成项目,我只有用强硬态度。我想我的领导都会认为我是一个能干活的人,但态度实在不好。每每,我超出预计的完成任务,最后拿到的却是缩了水的奖金,呵呵,其中的委屈有谁知道。
为朋友办事,耽误了我大量的时间。那些天,我每每想起Looking都非常着急。事情总算尘埃落地了,一回到电脑前,我就产生了要开发LJet的强烈愿望。我给自己定的第一个任务是CSG(实体构造学)。为什么是csg?原因只有一个,我喜欢。第一次看到csg的boolean效果是在2年前,当时我被这种效果震住了,当时我想我一定要自己实现一个。在深入开发LJet之前,我必须要作一件事情,那就是给自己定个规矩,程序风格。LJet的代码量大约要有1万行,在我作过的系统里它不算大,但它的调试难度却肯定是最大的。举个简单的例子,如果一个球面和一个立方体相互切割,会产生800多个polygon,如果其中的几个polygon出现问题,该如何定位?我首先想到的问题是程序代码的清晰度问题。在培训新兵的时候,我经常会问他们:你编写的代码是给谁看的?我给他们的答案是:你编写的代码是给其他程序员看的;其他程序员看不懂的代码就是废码;如果一段时间后自己都看不懂的代码可真就是乱码了。呵呵,我想不会有人有兴趣看LJet的代码,我提高程序的清晰度只是为了降低调试强度。我给自己定的目标是:程序的清晰度要达到伪代码级。为了实现这个目标,我使用了大量的宏。很多C程序员可能都犯过下面这个错误:
if ( a == b )
...
写成
if ( a = b )
...
这东西就像癌症一样。在头脑不清楚的时候,非常难于发现。往往调试了几个小时后,才发现自己犯了这么个愚蠢的错误。如果在LJet里有一、两个这样的错误,我看我就别干了。
#define JEQUAL( a, b ) ( ( a ) == ( b ) )
上面的这个宏可以解决这种问题。这是个小事情,确实是小事情;这会增加键盘的敲击量,确实非常费手指头。但我不敢冒险,当所有的小事情累积在一起,就会变成大事情。当感觉到代码不可调试、不可控制时,能作的恐怕只有放弃了。
我看过很多C程序员的代码,我发现他们对指针操作情有独钟。我不是说不应该使用指针,这也是不可能的。我只是说代码的书写方式上没完没了的->,恐怕也是灾难。在LJet里的指针操作是非常巨大的,可以肯定100%的函数入口参数需要指针,95%的返回值是指针。在这里存在着一个很重要问题,代码的语意问题。一个变量或函数的名字,就应该说明它的功能,特别是函数名。不恰当的函数名甚至会把意思完全弄反了。在我编写代码的时候,经常为了使名称语意清晰、一目了然而且完全具备单向性,花费大量精力。在LJet中所有的指针操作都是用宏替代的。现在LJet已经有3000多行代码了,但在C源文件中,没有一个->操作。
在这里我举一个例子。在LJet中有一个JBSPNODE结构,它是一个二叉树节点。同时它描述了两个方向,右节点是front,左节点是back。在二叉树的维护上,我们可能更喜欢使用left和right这样的术语,而在空间分割上我们可能更喜欢front和back这样的描述。
#define JBSPNODE_LEFT( p ) ( ( p )->left )
#define JBSPNODE_RIGHT( p ) ( ( p )->right )
#define JSBNODE_FRONT JBSPNODE_RIGHT
#define JSBNODE_BACK JBSPNODE_LEFT
这样代码的语意就非常清晰了。另外宏提供了一个一动而动全局的机会。例如LJET的polygon结构是JPOLYGON,它描述顶点的方式是使用一个顶点索引数组。
#define JPOLYGON_VERTEX( p, i ) ( ( p )->indices[ i ] )
在历遍polygon的所有edge时, 经常会是下面这样:
for ( UINT i = 0; i < JPOLYGON_COUNT( pPoly ); i++ )
{
JPOLYGON_INDEX_TYPE idx1, idx2;
idx1 = JPOLYGON_VERTEX( pPoly, i );
if ( JEQUAL( i, JPOLYGON_COUNT( pPoly ) - 1 ) )
idx2 = JPOLYGON_VERTEX( pPoly, 0 );
else
idx2 = JPOLYGON_VERTEX( pPoly, i + 1 );
}
但如果把JPOLYGON_VERTEX修改一下,代码就清晰多了:
#define JPOLYGON_VERTEX( p, i ) ( ( p )->indices[ i ] % JPOLYGON_COUNT( ( p ) ) )
for ( UINT i = 0; i < JPOLYGON_COUNT( pPoly ); i++ )
{
JPOLYGON_INDEX_TYPE idx1, idx2;
idx1 = JPOLYGON_VERTEX( pPoly, i );
idx2 = JPOLYGON_VERTEX( pPoly, i + 1 );
}
当然,为了提高效率可以声明一个新的顶点索引宏。
#define JPOLYGON_VERTEX( p, i ) ( ( p )->indices[ i ] )
#define JPOLYGON_VERTEX2( p, i ) ( ( p )->indices[ i ] % JPOLYGON_COUNT( ( p ) ) )
有时候把一个函数放到什么地方也是非常伤脑筋的事情。例如一个plane分割polygon的函数,是放在plane.c里还是放在polygon.c里?这可不是个小事情,当系统有几百个函数的时候,我希望从名字上就能定位它在哪个文件里。我一般使用主动动词法。例如,刚才的那个函数名是jPlaneSplitPolygon,那它肯定在plane.c里。
尽管在代码清晰度上煞费苦心,但LJet的调试强度还是令我意外。这也难怪,在调试BSP树的时候,整个文件都充满了递归、变相递归、连表操作、树操作,而且吞吐的数据量大的惊人。另外一个原因是,LJet在开发初期几乎不可能调试。调试基本上是在有数据的情况下进行的,但LJet是个扁平的系统构建,这些元素互相纠缠,缺一不可。在没有相当的代码的情况下,根本就运行不起来,更不用说看到结果了。LJet是在写了2000多行代码后,才开始调试的。在没有调试的情况下凭空构造系统,我称之为“盲写”(呵呵,又是俚语)。我认为,一个程序员特别是C程序员的“盲写”能力,是评价其技术水准的基本指标之一,当然,这不是在评价初级程序员。
经过和LJet的一番角斗,基于bsp的csg系统终于运行起来了。但令我费解的是,在极度切割的情况下(例如,一个cub和一个sphere进行boolean操作),会出现很多斑点。尽管我进行了polygon优化和polygon合并。但情况依然没有改善。数据量实在太大了,尽管没有使用最优polygon合并算法,一次合并也会合并120多个polygon。为了这些斑点,我和LJet整整僵持了2天。最后,当我看到一篇关于T-junctions的文章后,才知道问题所在。真是谢天谢地,我还以为这次又完蛋了。下面的图片是csg的boolean sub的结果,由于csg还在收缩调整阶段,因此结果还没有达到100%的满意。
我们家的宝宝两个月后就要出生了。昨天,我把耳朵贴在我爱人的肚子上对宝宝说:等你出来后,我会在电脑里给你建一个大毫斯(big house)。这时候他,砰,踢了我一脚,看来他还是个急性子,嫌我工作太慢。呵呵。