CSDN程序员怎样学数学:半路出家也能让编程是小菜一碟 ; 讨论:写程序到底需不需要懂数学?等相关新闻,拥有极高的点击率,也引来读者在新闻下方长篇的讨论。写程序到底需不需要懂数学?数学对于程序员来说到底重不重要?类似这样标题的讨论,已经像哪种编程语言更优秀一样,成为长争不熄的话题。 逆风编程精品 两届微软MVP获得者刘洪峰(网名:叶帆)认为,如果是一个真正的程序员,那么他(她)天生就应该喜欢学习数学才对。 因为在叶帆看来,学理科的人分两种不同的人群,一部分人以逻辑思维为主,数学、物理、编程成绩较好;另一部分人以记忆为基础,英语和化学优胜一筹。以叶帆为例,他个人已从事软件开发十余年,是“地地道道”的程序员,同时也很喜欢数学:大学里就曾参加过学校组织的计算机编程和高等数学的竞赛,取得了编程第一,高等数学第二的好成绩。“所以我认为真正的程序员,不存在应不应当学习数学的问题,只要工作需要,随时都可以深入学习。”叶帆如是说。 不过叶帆也承认高等数学与一般程序员的关系并不是很大,只需理解和实现即可。因为在实际工作中有时候很少用到高等数学的知识。叶帆也只是在开发隧道广告系 统中,对图像处理和г校正时用到一部分数学知识(主要是矩阵变换的知识),而且其中的大部分实现还是借鉴了现成的代码,数学知识本身反而并不需要深入了 解。听起来,叶帆的回答颇有矛盾辨证的感觉。 特殊群体的程序员(如搜索开发、图像处理等等)和研究所的研究员似乎可能需要更多的数学知识,至于一般程序员,叶帆认为“只要具备高中数学基础就已经足够。” 先前有国外著书,矛头直指早期的计算机先驱如约翰冯诺伊曼(John von Neumann)和阿兰图灵(Alan Turing),说他们本身就是理论数学家,把数学模型移植到计算机中,尤其是算法,从而导致从那时到现在计算机科学始终受到数学的制约。他还认为在算法 概念上编写软件不是什么好方法。对此,叶帆认为在编程没有涉及到高端技术层面上的时候,算法概念上的编写软件只是极少人的课题,因为面向对象的编程的思想已经深入人心。“当然如果你是一个科学家(或研究员),数学一定离你很近,甚至有可能数学是你工作的灵魂,而计算机代码仅仅是表达你思想的一种道具。”【文:呐不喊】 编后语: 记 得一位有着10多年开发经验的老程序员曾经说过:“所有的程序本质上就是逻辑。技术你已经较好地掌握了,但只有完成逻辑能力的提高,你才能成为一名职业程 序员。”他就象打了一个比方,你十八般武艺都很精通,但就是内力不够,所以永远都成不了武林顶尖高手。而这个内力对于程序员来说,就是你的数学内涵。 那么,计算机和数学的关系究竟怎样?程序员是否只有掌握了数学才能写出好程序?又该如何学数学?CSDN专题将与您一起讨论这方面的问题: 数学对于程序设计师来说到底重不重要?!类似这样标题的讨论,在网络上已经不知道被讨论多少次了。前两天又 在老同事小白的blog上看到了他的看法。以前正方总是喜欢拿算法与效率来表明数学很重要的立场,反方或是最近普遍的观点是要依照需求。两方都没错,我也 有一点小心得跟大家分享。 先来谈谈「数学」在大家的心中是长什么样子。我大学时念的是辅大应数,会选应数的原因是一、我的分数上不了 资工,二、应数又跟纯数不一样,是比较偏计算机应用的(事实上不是这么回事),应数的全名是应用数学。三、高中时一位要好且计算机很强的同学也是念辅大应 数,所以我就这样进了数学系。在一般人眼中是个很硬的科系,那几年全校1/2的名单中,数学系就占了一半。数学系所学的数学,跟一般人所会用到的数学不太 一样。除了几门工科必备的微积分、线性代数、机率统计外,剩下的都是高深且抽象的数学理论,像是高等微积分、高等代数、几何学(不是三角形、正方形那种简 单几何)、拓扑学等。这几门课程跟本像天书一样,非常的抽象(无法画在直角坐标系上),我能毕业也算是一种奇迹啊~~我真怀疑我那学念到博士班的同学们, 他们的脑袋是不是跟我长得不一样。 研究所顺利考上的向往已久的资工所,成为名符其实的本科系学生,本以为可以不用再玩数学了,但我发现我错了,是不用再玩那些抽久的高等数学没错,但线性代 数、机率统计、离散数学等…用了更多的数学,我想不出来有哪门资工研究所的课没用到数学的。而且你最后的硕士论文要写出来,数学更是不能少的。你以为玩网 络不需要数学?大错特错,里面一堆机率统计的东西。电机需要数学吗?当然需要!最基本的傅利叶转换就够搞死你了,所有工科的系所都逃不了数学的魔掌。就算 你到了管理学院,会计系要数学、经济系要数学、连心理系有些领域也需要数学。虽然所需要的数学不尽相同,但都在数学的领域里。我开始后悔当年没把数学念 好,博士班念到一半念不下去了,其中一个原因是我数学太烂了。 写程序需要数学吗?要看程序的目的?那我们就像讨论一个简单的程序,算出1加到100的总和。 完全以程序结果为导向的人,或是训练有素的程序女/男工,甚至有时连我都会很直觉的写出这样的程序: int sum = 0; for (int i=1; i<=100; i++) sum += i; 上面这个程序片段还算很容易让人一眼就看懂,可是我们明明国中时就学过了这种数列级数的算法了,怎么还会写出上面这么笨的程序呢? int sum = ((1 + 100) * (100 - 1 + 1)) / 2;或更精简的 int sum = (101 * 100) >> 2; 这个例子已经被说烂了,我们来来看另一个例子,计算1加到10000,奇数和偶数的总和。用循环的话,一样很直觉得就写出来了: int oddSum = 0; int evenSum = 0; for (int i=1; i<=10000; i++) { if (i % 2 == 0) evenSum += i; else oddSum += i; }很简单的程序吧!可是我们稍稍的用我们有12年(国小到高中)的数学背景想一下,你可以写出更精简的程序: int sum = (10001 * 10000) >> 2; int evenSum = 5001 * 5000; int oddSum = sum - evenSum;什么?看不懂?!sum应该知道怎么算出来吧?就刚刚第一个例子是1加到100,现在改加到10000而已。evenSum呢?简单的推 理一下,1到10000之间的偶数总和是是2+4+6+...+10000,把它们全部除以2的话会变成1+2+3+...+5000,所以1到 10000偶数的总和不就是1加到5000的两倍吗? 1加到5000是: (5001 * 5000) >> 2两倍就不用除那个2了,所以不就是上面那个算式了吗! 那1到10000奇数的总和不就是全部的总和减掉偶数的总合吗!稍微动一下脑袋,可以让你的程序变得很有效率。怕别人看不懂?是不会加个批注在程序代码里面喔! 相信聪明的你,很容易就可以分析出来这两个例子的两种写法,在效能上迶多大的差异,但这不是我在这里想要表答数学是如何如何增加效率的。我想要表答的是, 我们明明辛苦了12年,学了一堆的数学,为什么我们要放弃这样的基本训练?我们笑美国人的数学不好,请问你又用了多少的数学来帮助你的生活和你的工作?学 了又不用,那不如从小学开始就分科系好了,不喜欢数学、怕数学的,就选完全用不到数学的科系。 我今天不是要大家在写程序是时钻研那种算法、功式等,去计较那些在现在动不动在2GHZ, 3GHZ,双核心、四核心之下,所省下的那微小的效率。而是你的态度!你宁愿多打一些code,也不愿动一下脑筋,如果你的态度是这样子的话,那也是活得 下去啦,不过你的水平就不过如此而已。 你会反驳说,需要用到算法、要讲求效率时,我再去研究一下就好了,干嘛说的很严重似的。今天我们一时兴起,要去爬阳明山,没问题啊,那种程度的山,只要双 脚健全走得动的人都能爬。如果换成现在流行的登山步道呢?这需要一点点体力才行。如果你要去爬台湾百岳呢?合欢山的东峰算是最简单的吧?开车到山脚下,穿 个好一点的鞋子、好一点的衣服、多一点的体力,也还不是太大的问题。那爬玉山呢?虽然现在爬玉山已经很方便了,连行李都可以请人帮你背,但平常没有一些训 练,要爬上去不是那么容易的事。你要站在世界的最高点,去挑战圣母峰,那全世界没几个人办得到,而且办到的人事前可是经过了严格的训练。 你想把自己摆在什么位置?你想要成就到什么样的高度?如果你只想在小小的台湾,在二、三流的公司里,打打项目游击战,赚个还算可以的薪水,那的确,你不怎 么需要数学,连软件工程的理论也不太需要,最重要的唬弄客户的技术纯熟就可以了。去年去了101的37楼面试后,我才知道我了不起只爬到阿里山而已,要登 上MountainView这座山,我必需十倍努力才行。而这个努力不是我在面试前,看看什么教战手册、写写网络上的考古题我就能够通过的,而是必需把一 些数学的训练熟到变成很自然的反应才行。简单的问你就好了啦,上面那个用循环写的1加到10000的那个例子,如果10000改用n的话,那需要多少时 间,用大O(big O)来表示。如果你不能很快的推论出是O(n)的话,那你的履历连投都不要投,在37楼问的问题比这难多了,而且你没几分钟的时间可以作答。去年那次,是 我第二次后悔当初没把数学学好。 为什么Google会这么重视算法和效率?应该说世界级的大公司都重视,Yahoo、微软、YouTube…,因为你写的程序不是给几十个人、几百个人用 而已。而是同一时间有几百万,甚至上千万人使用。一个人慢0.1秒好了,一百万人就10万秒,超过一天耶。浏览一个网页,慢个几秒钟你都不能忍受了,更何 况是一天。你说能不计较算法和效率吗?! 你想过什么样的生活是你自己决定的,但你想要当个世界级的软件工程师的话,把学数学就当作是一种修行吧!不要怕没地方用,因为你时时刻刻都可以用到它。当你的修行到了某个程度,要挑战高山,就比别人容易多了。 好好好,不想深入就算了,但你不觉得放弃12年的数学训练很可惜吗?至少花点脑筋用一下嘛~~,大家都多少用一点,软件的水平就会慢慢提升了,你说是吗? 自从我读了Johnny von Neumann的传记,我已经为弥补我糟糕的数学技能花了15个月了.读了大量的数学书籍,不过呢,似乎我还有更多没有读.当然我会接着做的. 现在我就来告诉你这些. 这并不包括传统的智慧 首先:程序员不认为他们需要了解数学.我常常听到这样的话;我不知道还有会不同意这个的.甚至于以前是主修数学的程序员也告诉我他们真的不是常常使用到数学!他们说 更重要的是要去了解设计模式,面向对象原理,软件工具,界面设计,以及一些类似的东西. 你了解吗?他们完全正确.你不需要了解很多数学你就能做个很棒,很专业的程序员. 但是呢,同时你也不是真的需要知道如何来编程.我们要面对的是:有很多专业的程序员,他们认识到他们不是非常擅长数学,但他们还是寻找方法去提升. 如果你突然觉得自己好烂,周围的人都远远的超过你,你会怎么想呢?好,你可能会发现自己善于项目管理,或者人事管理,或者界面设计,或技术写作,或者系统 管理,还有许多其他程序员不必去精通的.你会开始堆积那些想法(因为工作永远干不完),当你发现一些你能掌握的东西时,你很可能会转移去全职的做这个工 作. 实际上,我认为有些东西你不需要了解,当目前你还能够赖以生存. 所以他们是对的:你不需要了解数学,并且没有她你也能过的很好. 但是最近我学到一些东西可能会让你也感到惊喜: 在你知道如何编程之后,数学更容易学会.实际上,如果你先学数学,然后半路出家做程序员的话,你会发现编程简直就是小菜一碟. 学校里教数学的方式都错了.仅仅是教学的方法错了,不是教数学本身错.如果你以正确 的方式学习数学的话,你会学的更快,记住这会更长,但对你作为一个程序员来说也更有价值. 哪怕了解一点点相关的数学知识就能让你写出可爱有趣的程序,否则会有些小难度.换 句话讲,数学是可以慢慢学的,只要你有时间. 没人能了解所有的数学,就是最棒的数学家也不是.数学领域正不断的扩展,当人们发明 新的形式去解决自己的问题时.一些给出的数学问题,也正如编程,不止一种方法可以去 解决他.你可以挑个你最喜欢的. 数学是......嗯,请别告诉别人我说过这个哈;当然我也不指望谁能邀请我参加这样的 派对,当我还活着的时候.但是,数学其实就是......我还是小声的说吧,听好了她其 实就是一种乐趣啦!) The Math You Learned (And Forgot) 你学到的数学(和你忘了的) 这儿是我能记得在学校学到的数学: 初中:数,数数,算术知识,初级代数("问题故事") 高中:代数,几何,高等代数,三角学,? (圆锥和极限) 大学:微积分,微分公式,线性代数,概率和统计,离散数学 上面那个关于高中数学课程单子上所列的,怎么来着?美国高中几乎都是这样的课程设置.我认为其他国家也会很相似的,除了那些在9岁之前就掌握了这些课程的学生.(美国人同时却在热衷于玩魔鬼卡车竞赛,虽然如此,整个来说也算不上什么大损失.) 代数?是的.没问题.你需要代数.和一些理解解析几何的知识.那些很有用,并且在以后 几个月里,你能学到一切你想要的,十拿九稳的.剩下的呢?我认为一个基本的介绍可能 会有用,但是在这上面花整个学期或一年就显得很荒谬了. 我现在意识到那个书单列表原是设计来准备给那些以后要当科学家和工程师的学生的.他们在高中里所教的数学课程并不是为你的编程生涯做准备的,简单的事实是多数的编程工作相比其他的工程师角色更加要求快速. 甚至于你打算当一名科学家或者一名工程师,我会发现这更加容易去学习和欣赏几何学和三角在你理解了什么是数学之后-- 数学它如何而来,如何而去,为何而生.不必去专研记住几何上的证明和三角恒等式.但是那确实是高中学校要求你必须去做的. 所以这样的书单列表不再有什么用了.学校教了我们不是最合适的数学,并且方式也不对.不奇怪程序员认为他们不再需要数学:我们学的大部分数学知识对我们的工作没什么大的帮助. The Math They Didn't Teach You 他们没有教到你的那部分数学 在真实的生活中,计算机科学家有规则的使用数学,对于上面单子里列的有点小小超过. 举个例子,你在中学里学的大部分数学是连续性的:也就是说,数学是真实的数字.而对于计算机科学家来说,他们所感兴趣的部分是占95%也许更多的离散性的:比如,关于整数的数学. 我打算在我以后blog中再谈一些在计算机科学,软件工程,编程,hacking,和其他常常迷惑的管理的之间的关键差异.我已经从Richard Gabriel的软件的模式这本书中洞察到一个无关细节的基本框架.如果你明显的等不下去的话,去读吧.是本不错的书. 到现在为止,不要让"计算机科学家"这个词困扰到你.它听上去很可怕,其实数学不是计算机科学家所独有的领域,你也能作为一个黑客自学它,并且能做的和他们一样棒.你作为一个程序的背景将会帮助你保持只关注那些有实践性的部分. 数学,我们用来建立计算模型的,大体上是离散的整数.这是普遍化的做法.如果正好今天你在看这篇博客,从现在起你正了解到更多的数学,并且你会认识到那样的普遍化是不对的.更多的,你将有信心认为可以忽略所有这些,并以你想要的方式自学. 对程序员来说,最有效的离散数学的分支是概率理论.这是你在学校学完基本算术后的紧接着的课.你会问,什么是概率理论呢?你就数啊,看有多少次出现满堂 彩?或者有多次是同花顺. 不管你思考什么问题如果是以"多少种途径..."或"有多大几率的...",那就是离散问题.当他发生时,都转化成"简单"的计数.抛个硬币看看...? 毫无疑问在他们教你基本的计算用法后他们会教你概率理论. 我还保存着大学里的离散数学课本.可能他只占了三分之一的课程,但是它却涵盖了我们几乎每天计算机编程工作大部分所使用到的数学. 也真是够奇怪的,我的教授从没告诉我数学是用来干吗的.或者我也从来没有听说过.种种原因吧.所以我也从没有给以足够的注意:只是考试及格然后把他们都忘 光,因为我不认为她还和编程有啥关系.事情变化是我在大学学完一些计算机科学的课程之后,也许是25%的课程.可怜的人!我必须弄明白什么对于自己来说是 最重要的,然后再是向深度发展. 我想,如果每门数学课都花上整整一周的时间,而只是介绍让你如何入门的话,那将非常不错,这是最有意思的一种假设,那么你知道了你正学习的对象是哪种怪物了.怪物,大概对每一门课都合适. 除了概率和离散数学外,还有不少其他的数学分支,可能对程序员相当的有用,学校通常不会教你的,除非你的辅修科目是数学.这些数目列表包括: 统计学,其中一些包括在我的离散数学课里,她的某些训练只限于她自身.自然也是相当重要的,但想学的话不需要什么特别的入门. 代数和线性代数(比如,矩阵).他们会在教完代数后立即教线性代数.这也简单,这但相当多的领域非常有用,包括机器学习. 数理逻辑.我有相当完整的关于这么学科的书没有读,是Stephen Kleene写的,Kleene closure 的发明者,我所知道的还有就是Kleenex.这个就不要读了.我发誓我已经尝试了不下20次,却从没有读完第二章.如果那位牛掰有什么更好的入门建议的 话可以给我推荐,给个回复.虽然,这明显是非常重要的一部分. 信息理论和柯尔莫戈洛夫复杂性理论.真不可思议,不是么?我敢打赌没哪个高中会教你其中任何一门课程.她们都是新兴的学科.信息理论是(相当相当相当相当 难懂)关于数据压缩,柯尔莫戈洛夫复杂性理论是(同样非常难懂)关于算法复杂度的.也就是说,你要把它压缩的尽量小,你所要花费的时间也就变的越长,同样 的,程序或数据结构要变得多优雅也有同样的代价.他们都很有趣,也很有用. 当然,也有其他的一些因素,某些领域是重复的.也拿来说说吧:你所发现有用的那部分数学,不同于那些你在学校里认为有用的数学. 那微积分呢?每个人都学它,所以它也一定是重要的,不对吗? 好吧,微积分实际上是相当容易的.在我学习它之前,它听上去好像是世界上最难的一件事,好像和量子力学差不多.量子力学对我来说真的不是那么容易理解,但 是微积分却不是.在我意识到程序员能够快速的学习数学时,我拿起一些微积分课本用一个月通读了整本书,一个晚上读一小时. 微积分都是关于连续统的 -- 变化的比率, 曲线的面积, 立体的体积.是些有用的东西,但是实际细节却包含大量的记忆量并且枯燥,作为一个程序员来说根本不需要这些. 更好的方法是从整体上了解那些概念和技术,在必要的时候再去查询那些细节. 几何,三角,微分,积分,圆锥曲线,微分方程,和他们的多维和多元 -- 这些都有重要的应用.不过这时候不需要你去了解它们.这大概不是个好注意让你年复一年的去做证明和它们的练习题,不是吗?如果你打算花大量的时间去学习数学,那也是和你生活相关的部分. 学习数学的正确方法 正确学习数学的方法是广度优先,而非深度优先.你需要生存在空间里,学习事物的名字,区分出什么是什么. 以透视的方法来对待的话,考虑用用长整除.(汗一个,感觉译的不准确)现在就举起你的手如果你能在纸上做长整除.手吗?谁呢?我可不这么认为. 回头看看在学校里学过的长除法,要是不让你觉得烦恼和愤怒才怪.当然,这是显然的,但你不一定要自己亲自去做,因为很容易用计算器来做,即使你不幸在一座没有电力的荒无人烟的小岛上.你起码还有个计算器,在的手表上,补牙的什么东东,或其他什么上面. 为什么他们还教你这些呢?为什么我们感到含混心虚讷,如果我们不能记住怎样去做?这不是好像我们需要再次知道她.除此以外, if your life were on the line,你可以运用任意大的数来做长除法.相象你被囚禁在第三世界的地牢里,那儿的独裁者是 不会放你出来的,除非你计算出 219308862/103503391.你会怎么做呢?好吧,很容易.你开始从分子减去分母,直到不能再减 只剩余数为止.if pressed,你可以想个办法估计好作为十进制的余数反复来减(这种情况下,0.1185678219,Emacs M-x calc 告诉我的.够精确了! ) 你也许能明白因为你知道除法就是反复的减.对除法概念的直觉是根深蒂固的. 学习数学的正确方法是忽略实际的算法和证明,对于大部分情况来说, ...:他们的名字,他们的作用,他们计算的大致步骤, (有时是)谁发明了他们,发明了多久了,他们的缺陷是什么,和他们相关的有什么.把数学当文科来学. 为什么讷?因为第一步应用在数学上的是问题的确定.如果你有一个问题去解决,并且如果你没有头绪如何开始, 这将花费你很长的时间来弄明白.但如果你知道这是个变异的问题,或者是一个凸优化问题,或者一个布尔的逻辑问题, 然后你起码能知道从哪着手开始寻找解决方案. 现在有许许多多的数学技术和整个的学科分支.如果你不知道组合逻辑是什么,甚至连听都没听说过, 那么你是不可能意识到在组合逻辑中可以找到的解决答案的问题的,难道你会么? 但那实在是个大新闻哪,因为阅读这些领域,学习实际算法,建模和计算结果的方法,记住这些名字都是容易的.在学校里他们教你链式法则,你也能回忆起他们并 能运用在考试题上,但有多少学生能真正的了解他们到底意味着什么呢? 所以当他们遇到变种的链式问题时他们就不懂得如何运用公式了.让人感到讽刺的是,了解这是什么比记住如何运用公式更为容易.链式法则仅仅是如何对链式函数 求导的意思,函数 x() 引用函数 g() ,你要求导 x(g()) .好,程序员知道所有和函数相关的;我们每天都使用他们,所以现在这比过去在学校更加容易能够相象出问题. 这就是为什么我认为他们以错误的方式在教数学. 对大多数高中毕业生来说,他们专门教授的内容不是可以靠经验来证明数学是如何有用的,他们教的那些恰恰是非经验式的内容.在你学习如何求导和做积分之前,你将要学习如何计数,怎样编程. 我认为学习数学最好的方法是每天花15到30分钟逛维基百科.那上面有数千数学分支的相关文章. 可以从一些你感兴趣的文章着手(比如,炫理论,或者,傅立叶变换,或者张量理论,就是能冲击你相象力的东西) 阅读.如果有什么你不理解的,就去了解那些链接.如此这般直到你累到不行. 几个月后,这么做会纵向扩展你的数学知识面.比如,你会发现一些模式--比如,数学的每个分支看上去都包括了一个有着复杂的多元版本的变量,所以线性代数 将会琢建爬满你的 书单列表,直到你强迫自己学会他实际上是怎样工作的,你要下载个电子书或买本书,直到你 能从中找到乐趣. 藉着维基百科,你也能快速的找到一条了解数学基本原理的途径,条条大道通罗马.在某些领域,数学几乎总是形式化我们的"常识",所以我们能减少或证明那些 领域里的新事物.对数学本身的研究就是无止境而且令人着迷的:构造形式模型本质的能力,证明,自明的系统, 规则表示,信息,和计算. 符号是个很重大的但很快被放弃的东西.数学符号是关闭你通往另一个世界的符咒.即使你熟悉累加,积分,多项式,指数,等等,如果你看到一堆符号堆彻的异常复杂时,你就把他实现的功能简单的当成一个原子操作好了,不要深究太多. 然而,从观察数学来说,尝试着明白人们正在试图解决的问题(那些已被证明了的问题某天也许会对你有实际用途), 你会开始在符号中看到相同的类型,你也不再排斥他们.比如,累加符号(大写符号-西格马)或者 product sign(大写符号-pi)起初看上去让人心里没底,即时你了解了他们的基本原理.但如果你是个程序员,你会认识到他仅仅是个循环:一个累加值,一个累 乘.积分是一段连续曲线的相加,所以那不会让你郁闷太久. 一旦你习惯了数学的许多分支,和许多不同的符号的格式,你就走在了解许多数学知识的 路上了.因为你不再害怕,你将会发现问题,其实他们会自动跳到你面前."嗨,"你会思索,"我 了解这个.这是乘法符号!" 这样你就能扔掉计算器了.有一个充满相象的计算器比如 R,Matlab,Mathematica,甚或是 支持向量机的C语言库.但几乎所有有用的数学都是重型自动机,所以你能够让一切都变的自动化. When Are Exercises Useful? 练习有啥用处呢? 在做了几年的业余数学爱好者之后,你打算做更多的数学,甚至你从没碰过铅笔和纸.比如, 你会一直看到多项式,所以最后你会耳濡目染的做起多项式的运算.同样的,对数,根,超越数,和其他到处出现的基本数学原理. 我还是得到了一种感觉我要亲手做许多的练习题.我正在寻找一种能够跟着证明步骤的方法,比如使用一种"貌似可信的测试",如果他们的结果看上去或多或少是 对的,然后我就会拍拍屁股过去了.但如果我看着的那个说明我从来没听说过,亦或看上去是错的或不可能的情况,我就会挖更多的东西了. 这很像读程序源代码,不是么?当你读某人的代码你不需要手动模拟整个程序状态;如果你知道计算过程大致会发生什么情形,你能理智简单检测出结果.举个例 子,如果结果是个列表,他们返回一个标量,可能你会挖的更深一点.但正常情况下你能扫描源代码几乎是以你阅读英文文本的速度(有时仅仅是速度上),并且你 自信你理解了全部状态,同时你也许会发现任何真正令你震惊的错误。 我认为那就是数学爱好者(数学家和真正的数学迷)怎样读数学论文的,或者任何包含了许多数学的旧论文.他们做了同样的分类检查,正如在你读代码的时候所做的,但是不只是这些,除非他们不想把作者的观点扳倒. 照那样说法,我还是偶尔做数学练习.如果那些(比如代数和线性代数)又不停的跑过来,然后我就开始做些练习去确定我是真正的理解她了. 但我要强调这点:不要让练习使你分心.如果一个练习(甚或是一篇特别的文章或章节)开始让你烦恼,那就暂时丢一边继续前进.该跑路就坚决跑路.让你的直觉引导你.你会学的更多,更快,你的信心也会随之增长. 这些怎样才能帮到我? 也许不是--不能立刻奏效.但确实能帮助提升你的逻辑推理能力;好比是在体育馆做练习,你整体的能力会提升如果你每天都做一点的话. 对我来说,我已经注意到一些我已经感兴趣的领域(包括人工智能,机器学习,自然语言处理,和模式识别)大量的使用到数学.如我已经挖的有点深度的领域,我 已经发现他们使用的数学不再比我在中学的学到的数学还要更难;大部分来说仅仅是不同领域.不是更难了, 并且学习使我能写(或者是在我自己的代码里使用)神经网络,基因算法,贝页斯分类器,集群算法,图像识别,和其他时髦的东西能产生很酷的应用.我常向我的 朋友显宝. 我已经渐渐意识到这点,当别人给我看一篇包含了数学符号的文章我不再像突然冒了一身冷汗:组合,微分,真值表,定列式,无限系列,等等.那些数学符号现在 变得容易相处了,但(像编程语言的语法)一开始的话多少还是有点让人感到有些怪异.现在我能更好的理解了,当我一点不知道正在说什么时,也不再感到自己是 个不懂数学的人了.因为我知道自己是能够弄明白的. 那很好. 我会继续加油做的更好滴.我还有不少活头,有好多书和文章要读.有时我会花整个周末来读数学书,有时会数周都不再思索她.也和其他兴趣一样,如果你单纯的信任她你就会有兴趣,也能更容易的消磨时光,你可以经常一点点的尝试应用你觉得有趣的并且从中获益. 数学在计算机图形学中的应用 Greg Turk, August 1997 “学习计算机图形学需要多少的数学?”这 是初学者最经常问的问题。答案取决于你想在计算机图形学领域钻研多深。如果仅仅使用周围唾手可得的图形软件,你不需要知道多少数学知识。如果想学习计算机 图形学的入门知识,我建议你读一读下面所写的前两章(代数,三角学和线性代数)。如果想成为一名图形学的研究者,那么对数学的学习将是活到老,学到老。如 果你并不特别喜欢数学,是否仍有在计算机图形学领域工作的机会?是的,计算机图形学的确有一些方面不需要考虑太多的数学问题。你不应该因为数学成绩不好而 放弃它。不过,如果学习了更多的数学知识,似乎你将在研究课题上有更多的选择余地。对于在计算机图形学中哪些数学才是重要的还没有明确的答案。这领域里不 同的方面要求掌握不同的数学知识,也许兴趣将会决定了你的方向。以下介绍我认为对于计算机图形学有用的数学。别以为想成为一名图形学的研究者就必须精通各 门数学!为了对用于图形学的数学有一个全面的看法,我特地列出了很多方面。但是许多研究者从不需要考虑下面提到的数学。最后,虽然读了这篇文章后,你应该 会对数学在计算机图形学中的应用有所了解,不过这些观点完全是我自己的。也许你应该阅读更多的此类文章,或者至少从其他从事计算机图形学工作的人那里了解 不同的学习重点。现在开始切入正题。代数和三角学对于计算机图形学的初学者来说,高中的代数和三角学可能是最重要的数学。日复一日,我从简单的方程解出一 个或更多的根。我时常还要解决类似求一些几何图形边长的简单三角学问题。代数和三角学是计算机图形学的最基础的知识。那么高中的几何学怎么样呢?可能让人 惊讶,不过在多数计算机图形学里,高中的几何学并不经常被用到。原因是许多学校教的几何学实际上是如何建立数学证明的课程。虽然证明题对提高智力显然是有 效的,但对于计算机图形学来说,那些与几何课有关的定理和证明并不常被用到。如果你毕业于数学相关领域(包括计算机图形学),就会发现虽然你在证明定理, 不过这对开始学习图形学不是必要的。如果精通代数和三角学,就可以开始读一本计算机图形学的入门书了。下一个重要的用于计算机图形学的数学——线性代数,多数此类书籍至少包含了一个对线性代数的简要介绍。推荐的参考书: Computer Graphics: Principles and Practice James Foley, Andries van Dam, Steven Feiner, John Hughes Addison-Wesley [虽然厚重,可是我很喜欢] 线性代数线性代数的思想贯穿于计算机图形学。事实上,只要牵涉到几何数值表示法,就常常抽象出例如x,y,z坐 标之类的数值,我们称之为矢量。图形学自始至终离不开矢量和矩阵。用矢量和矩阵来描述旋转,平移,或者缩放是再好不过了。高中和大学都有线性代数的课程。 只要想在计算机图形学领域工作,就应该打下坚实的线性代数基础。我刚才提到,许多图形学的书都有关于线性代数的简要介绍——足够教给你图形学的第一门课。推荐的参考书: Linear Algebra and Its Applications Gilbert Strang Academic Press 微积分学 微 积分学是高级计算机图形学的重要成分。如果打算研究图形学,我强烈建议你应该对微积分学有初步认识。理由不仅仅是微积分学是一种很有用的工具,还有许多研 究者用微积分学的术语来描述他们的问题和解决办法。另外,在许多重要的数学领域,微积分学被作为进一步学习的前提。学习了基本代数之后,微积分学又是一种 能为你打开多数计算机图形学与后继的数学学习之门的课程。微积分学是我介绍的最后一个中学课程,以下提及的科目几乎全部是大学的课程。 微分几何学微分几何学研究支配光滑曲线,曲面的方程组。如果你要计算出经过某个远离曲面的点并垂直于曲面的矢量(法向矢量)就会用到微分几何学。让一辆汽车以特定速度在曲线上行驶也牵涉到微分几何学。有一种通用的绘制光滑曲面的图形学技术,叫做“凹凸帖图”,这个技术用到了微分几何学。如果要着手于用曲线和曲面来创造形体(在图形学里称之为建模)你至少应该学习微分几何学的基础。推荐的参考书: Elementary Differential Geometry Barrett O'Neill Academic Press 数值方法几乎任何时候,我们在计算机里用近似值代替精确值来表示和操作数值,所以计算过程总是会有误差。而且对于给定的数值问题,常常有多种解决的方法,一些方法会更块,更精确或者对内存的需求更少。数值方法研究的对象包括“计算方法”和“科学计算”等等。这是一个很广阔的领域,而且我将提及的其他几门数学其实是数值方法的一些分支。这些分支包括抽样法理论,矩阵方程组,数值微分方程组和最优化。推荐的参考书: Numerical Recipes in C: The Art of Scientific Computing William Press, Saul Teukolsky, William Vetterling and Brian Flannery Cambridge University Press [这本参考书很有价值可是很少作为教材使用] 抽 样法理论和信号处理在计算机图形学里我们反复使用储存在正规二维数组里的数字集合来表示一些对象,例如图片和曲面。这时,我们就要用抽样法来表示这些对 象。如果要控制这些对象的品质,抽样法理论就变得尤为重要。抽样法应用于图形学的常见例子是当物体被绘制在屏幕上时,它的轮廓呈现锯齿状的边缘。这锯齿状 的边缘(被认为是“混淆”现象)是非常让人分散注意力的,用抽样法中著名的技术例如回旋,傅立叶变换,空间和频率的函数表示就能把这个现象减少到最小。这些思想在图像和音频处理领域是同样重要的。推荐的参考书: The Fourier Transform and Its Applications Ronald N. Bracewell McGraw Hill 矩 阵方程组计算机图形学的许多问题要用到矩阵方程组的数值解法。一些涉及矩阵的问题包括:找出最好的位置与方向以使对象们互相匹配(最小二乘法),创建一个 覆盖所给点集的曲面,并使皱折程度最小(薄板样条算法),还有材质模拟,例如水和衣服等。在图形学里矩阵表述相当流行,因此在用于图形学的数学中我对矩阵 方程组的评价是很高的。推荐的参考书: Matrix Computations Gene Golub and Charles Van Loan Johns Hopkins University Press 物 理学物理学显然不是数学的分支,它是自成一家的学科。但是在计算机图形学的某些领域,物理学和数学是紧密联系的。在图形学里,牵涉物理学的问题包括光与物 体的表面是怎样互相影响的,人与动物的移动方式,水与空气的流动。为了模拟这些自然现象,物理学的知识是必不可少的。这和解微分方程紧密联系,我将会在下 一节提到微分方程。 微 分方程的数值解法我相信对于计算机图形学来说,解微分方程的技巧是非常重要的。像我们刚才讨论的,计算机图形学致力于模拟源于真实世界的物理系统。波浪是 怎样在水里形成的,动物是怎样在地面上行走的,这就是两个模拟物理系统的例子。模拟物理系统的问题经常就是怎样解微分方程的数值解。请注意,微分方程的数 值解法与微分方程的符号解法是有很大差异的。符号解法求出没有误差的解,而且时常只用于一些非常简单的方程。有时大学课程里的“微分方程”只 教符号解法,不过这并不会对多数计算机图形学的问题有帮助。在对物理系统的模拟中,我们把世界细分为许多表示成矢量的小元素。然后这些元素之间的关系就可 以用矩阵来描述。虽然要处理的矩阵方程组往往没有很精确的解,但是取而代之的是执行了一系列的计算,这些计算产生一个表示成数列的近似解。这就是微分方程 的数值解法。请注意,矩阵方程的解法与微分方程数值解法的关系是很密切的。 最优化在计算机图形学里,我们常常为了期望的目标寻求一种合适的描述对象或者对象集的方法。例如安排灯的位置使得房间的照明看起来有种特殊的“感觉”,动画里的人物要怎样活动四肢才能实现一个特殊的动作,怎样排版才不会使页面混乱。以上这些例子可以归结为最优化问题。十年前的计算机图形学几乎没有最优化技术的文献,不过最近这个领域越来越重视最优化理论。我认为在计算机图形学里,最优化的重要性将会日益增加。 概率论与统计学计算机图形学的许多领域都要用到概率论与统计学。当研究者涉足人类学科时,他们当然需要统计学来分析数据。图形学相关领域涉及人类学科,例如虚拟现实和人机交互(HCI)。另外,许多用计算机描绘真实世界的问题牵涉到各种未知事件的概率。两个例子:一棵成长期的树,它的树枝分杈的概率;虚拟的动物如何决定它的行走路线。最后,一些解高难度方程组的技巧用了随机数来估计方程组的解。重要的例子:蒙特卡罗方法经常用于光如何传播的问题。以上仅是一部分在计算机图形学里使用概率论和统计学的方法。 计算几何学计算几何学研究如何用计算机高效地表示与操作几何体。典型问题如,碰撞检测,把多边形分解为三角形,找出最靠近某个位置的点,这个学科包括了运算法则,数据结构和数学。图形学的研究者,只要涉足创建形体(建模),就要大量用到计算几何学。推荐的参考书: Computational Geometry in C Joseph O'Rourke Cambridge University Press [大学教材] Computational Geometry: An Introduction Franco Preparata and Michael Shamos Springer-Verlag [很经典,不过有点旧了] 总 结:数学应用和数学理论对于图形学来说,以上提到的许多数学学科都有个共同点:比起这些数学的理论价值,我们更倾向于发掘它们的应用价值。不要惊讶。图形 学的许多问题和物理学者与工程师们研究的问题是紧密联系的,并且物理学者与工程师们使用的数学工具正是图形学研究者们使用的。多数研究纯数学理论的学科从 不被用于计算机图形学。不过这不是绝对的。请注意这些特例:分子生物学正利用节理论来研究DNA分 子动力学,亚原子物理学用到了抽象群论。也许有一天,纯数学理论也能推动计算机图形学的发展,谁知道呢?有些看来重要的数学实际上在计算机图形学里不常被 用到。可能拓扑学是此类数学中最有意思的。用一句话来形容拓扑学,它研究油炸圈饼与咖啡杯为什么在本质上是相同的。答案是他们都是只有一个洞的曲面。我们 来讨论一下拓扑学的思想。虽然曲面是计算机图形学的重要成分,不过微分几何学的课程已经涵盖了多数对图形学有用的拓扑学知识。微分几何学研究曲面的造型, 可是拓扑学研究曲面的相邻关系。我觉得拓扑学对于图形学来说几乎没用,这是由于拓扑学关心抽象的事物,而且拓扑学远离了多数图形学的核心——三维欧氏空间的概念。对于图形学来说,拓扑学的形式(符号表示法)是表达思想的简便方法,不过图形学很少用到抽象拓扑学的实际工具。对图形学来说,拓扑学像一个好看的花瓶,不过别指望它能立即带给你回报。有人曾经这么问我,计算机图形学是否用到了抽象代数(群论,环,等等….)或者数论。我没怎么遇到过。和拓扑学一样,这些学科有很多美好的思想。可是很不幸,这些思想很少用于计算机图形学。 今天我们所要讨论的是平面。对于3D游戏制作来说,平面就像是直线在2D游戏制作中的地位一样,它可以帮你完成各种各样的任务。平面被定义为一个无 限大,无限薄的空间薄片,就像一张大纸。组成模型的各种三角形存在于各自的平面中。当你有了可以用来表示一个3D空间薄片的平面的时候,你就可以执行各种 运算,就像点、多边形的分类和剪裁。 因此,你怎样来表现平面呢?最好的方式就是从定义3D中平面的等式中构建一个结构。这个等式为:ax + by + cz + d = 0 那么这些数值代表什么呢?三元组表示平面的法线。从概念上讲,法线是一个直接穿出平面的单位矢量。完整的数学定义是,平面上的法线是一个跟该平面上所有的点都垂直的矢量。 等 式中的d表示从平面到原点的垂直距离。这段距离的长度就是从原点出发,向平面作垂直直线直到跟平面相交,所得到的线段的长度。最后的三元 组 我 给大家看的所有的图片都是经过严密组织的(呵呵,有点吹牛,ogdev上的朋友们不要笑话我),由于我们必须在2D画面中表示3D平面,因此图片中的3D 平面都将被画成一条直线放到边缘,这可以使图形的绘制变得简单。如果有其他更容易的方式可以在2D画面中表现3D图片的无限性的话,请大家告诉我,呵呵, 好让我也学习一种新的方法。 下面是平面的2个例子。第一个平面的法线是背离原点的,因此d是负数(如果你没弄明白的话,可以自己 尝试一些取样值,看得到的d是不是负数)。第二个平面的法线是朝向原点的,因此d为正数。大家可能已经猜出来了,如果我们的平面穿过原点,那么将会怎样? 对了,d的值为0。图5.13和图5.14显示了这些关系: 图5.13 法线背离原点,d为负数 图5.14 法线朝向原点,d为正数 大家应该注意到一个重点,那就是从技术上说,要使平面等式ax + by + cz + d = 0有效,法线不一定是单位长度的。由于如果法线是单位长度的话,事情处理起来就好办一些,因此本书上的法线都是单位长度的。 基本的plane3结构定义如下: 表5.14 The plane3 structure ======================================================= struct plane3 { point3 n; // Normal of the plane float d; // Distance along the normal to the origin plane3( float nX, float nY, float nZ, float D) : n( nX, nY, nZ ), d( D ) { // All done. } plane3( const point3& N, float D) : n( N ), d ( D ) { // All done. } // Construct a plane from three 3D points plane3( const point3& a, const point3& b, const point3& c); // Construct a plane from a normal direction and // a point on the plane plane3( const point3& norm, const point3& loc); // Construct a plane from a polygon plane3( const polygon plane3() { // Do nothing } // Flip the orientation of the plane void Flip(); }; ======================================================= 用 平面上给出的3个点构造一个平面是一件很简单的任务。你只需要使用由三个点组成的两个矢量 ( 表5.15 用平面上的3个点构造一个平面 ========================================= inline plane3::plane3( const point3& a, const point3& b, const point3& c ) { n = (b-a)^(c-a); n.Normalize(); d = -(n*a); } ========================================= 如果你已经有了平面的法线和一个点,那么第一步就可以省略了。见表5.16 表5.16 用平面上的一个点和法线构造一个平面 ========================================== inline plane3::plane3( const point3& norm, const point3& loc) : n( norm ), d( -(norm*loc) ) { // all done } ========================================== 最后,使用一个给出的带Point3元素的多边形构造一个平面。这个过程其实是从多边形中先取得3个点,然后使用上面给出的构造器而已。见表5.17: 表5.17 使用给出的polygon ========================================== inline plane3::plane3( const polygon { point3 a = poly.pList[0]; point3 b = poly.pList[1]; point3 c = poly.pList[2]; n = (b-a)^(c-a); n.Normalize(); d = -(n*a); } ========================================== 这里有一点很重要。那就是你的n边多边形上的点是否共面。如果有一些点跟其它点不共面的话,那么就会产生问题。这就是使用三角形来表现几何的一个优点—三点确定一个平面。 定义跟平面的位置关系 在平面上,还有一个很重要的操作就是定义跟平面相关的点的位置。如果你要把点代入到平面的等式,那么就可能分为三类:第一类是在平面的前面;第二类是在平面的后面;还有一类是跟平面共面。平面的前面就是该平面的法线出来的一面。 在这里,精度再一次被要求。由于不再是理论上的操作,因此平面不可能是无限薄,我设定每一个平面都具有epsilon(该值你可以任意的想象)的厚度。 那 么你如何确定跟平面相关的点的方向呢?其实很简单,你只要将该点的x,y,z的值带入到平面的等式,然后根据得到的结果判断点的方向:如果该数值是0(或 者通过加上、减去epsilon后趋近于0),就说明该点满足平面的等式,那么就是位于平面上,称为共面的点;如果该数值比0大,那么就是表示你从原点出 发沿着平面法线的方向到达该点的路径要比到达平面的路径远,说明该点在平面的前面;如果该数值小于0,那么该点就在平面的后面。注意,等式的前三项可以简 单的看为输入的矢量和平面的法线的点乘积。图5.15是该操作的可视化表示,而表5.18则是该操作的代码: 图5.15 跟平面相关的点的分类 表5.18 :plane3::TestPoint =========================================== // Defines the three possible locations of a point in // relation to a plane enum ePointLoc { ptFront, ptBack, ptCoplanar }; // we're inlining this because we do it constantly inline ePointLoc plane3::TestPoint( point3 const &point ) const { float dp = (point * n) + d; if(dp > EPSILON) { return ptFront; } if(dp < -EPSILON ) { return ptBack; } return ptCoplanar; // it was between EP and -EP } =========================================== 一 旦你有了对点进行分类的代码,那么对其他类型(比如多边形)的分类也变得微不足道了,请看表5.19的代码。不过这里可能存在四种结果,第一种是多边形在 平面的前面;第二种是多边形在平面的后面;第三种是多边形跟平面完全重合;还有一种是多边形的一部分在平面的前面,另一部分则在平面的后面,我们称这种情 况为多边形切割平面;当然这里的切割只是一个术语,实际上元素不可能切割任何东西。 表5.19 多边形分类代码 ================ // Defines the four possible locations of a point list in // relation to a plane. A point list is a more general // example of a polygon. enum ePListLoc { plistFront, plistBack, plistSplit, plistCoplanar }; ePListLoc plane3::TestPList( point3 *list, int num ) const { bool allfront=true, allback=true; ePointLoc res; for( int i=0; i { res = TestPoint( list ); if( res == ptBack ) { allfront = false; } else if( res == ptFront ) { allback = false; } } if( allfront && !allback ) { // All the points were either in front or coplanar return plistFront; } else if( !allfront && allback ) { // All the points were either in back or coplanar return plistBack; } else if( !allfront && !allback ) { // Some were in front, some were in back return plistSplit; } // All were coplanar return plistCoplanar; } =============== Back-face Culling(后面-正面选择?) 既然你已经知道了如何定义点跟平面的关系,你现在可以进行Back-face Culling了,这是3D图形最基本的优化技术之一。 假 设你有一个三角形,它的元素排列顺序是采用当前比较流行的方式,即从三角形的前面看三角形,元素是按照顺时针方向排列。Back- face Culling允许你可以在这种三角形定义方式下,使用平面等式剔除那些背离你(看不到)的三角形。从概念上说,任何一个封闭的网格,例如立方 体,都会存在一种情况,即有一些三角形面对你,而有一些三角形则背离你。你必须知道,那些背离你的多边形你是永远都看不到的,他们往往被面对你的那些三角 形挡住了。当然,如果你被允许可以查看立方体的内部,这个说法就不成立了。但是,如果你确实想优化你的引擎,那么这一点绝不允许发生。 你 可以使用平面等式先判断一下那些三角形中哪些是面对照相机(camera),哪些是背离照相机,然后把那些背离照相机的三角形抛弃掉,而不需要在屏幕上绘 制所有的三角形。那么是如何来完成这个工作的呢?给出三角形的三个点,你就可以定义出一个这个三角形所在的平面。由于你已经知道这个三角形元素的排列是按 照顺时针方向的,你也知道如果我们把元素按照顺序代入到平面构造器中,我们就可以得到平面的法线,它将位于三角形的前方。这时候,如果你把照相机所在的位 置看成一个点的话,那么你所需要做的所有工作就是进行点-平面测试。如果照相机的点在平面的前面,那么这个三角形就是可见的,因此它应该被绘制。 这 里有一个最佳的判定方案。因为你已经知道了位于平面中的三个点(三角形的三个点),所以你只需要知道平面的法线就行了,而不需要整个平面等式。要进行 back-face cull,只需要把照相机的位置减去三角形的一个点,并把得到的这个结果矢量同平面的法线进行点乘积。如果得到的结果大于0,那么就 说明视点(照相机的位置)在三角形的前面。图5.16可以帮助解释这一点。 图5.16 back-face dulling的一个可视化例子 实际上,3D加速器本身就可以执行back-face culling,因此手工进行back-face dulling已经越来越少了。不过,这些信息对那些不打算使用Direct3D工具的自行开发的3D引擎来说,是很有用的。 Clipping Lines(剪裁直线?) 你现在需要做的一件事情就是获得两个点(a 和 b),这两个点必须分居在平面的两边,形成一条线段,这样你就可以找到线段和平面的相交点。 这一点很容易做到。想想这些参数,点a可以被认为是时间为0的时候的一个点,而点b则可以被认为是时间为1的时候的一个点 ,那么你可以发现,相交处的这个点一定是在这两者之间的某个地方。 分别获得a和b同 平面法线的点乘积,然后使用它们和平面参数d的相反数,你就可以得到scale值(当直线同平面相交的时候,这个值在0到1之间,它是用来定义实际位置的 参变量)。将这个值代入到直线参变量等式,你就可以得到相交点的位置。图5.17是以上文字的图形表示,表5.20是它的代码: 图5.17 得到平面和直线的相交点 表5.20: plane3::Split =========================================== inline const point3 plane3::Split( const point3 &a, const point3 &b ) const { float aDot = (a * n); float bDot = (b * n); float scale = ( -d - aDot) / ( bDot - aDot ); return a + (scale * (b - a)); } =========================================== Clipping Polygons(剪裁多边形?) 既 然你已经能够剪裁直线,那么现在你也能够剪裁多边形。用平面剪裁多边形是一个常用的操作。你需要准备一个多边形和一个平面,进行这个操作之后你将得到一个 多边形,它是那个输入多边形的一部分,就是在平面前面的那部分。从概念上来说,你也可以理解成平面将在它后面的多边形切掉了,只留下在它前面的一部分。 在 剪切中主要应用的就是剪裁多边形。如果一个多边形位于某一个位置,即它的一部分在屏幕上,一部分不在屏幕上的时候的那个位置,你就需要对此多边形进行剪 裁,以便你只需要绘制在屏幕上的那部分多边形就可以了。如果绘制不在屏幕上的那部分多边形可能会破坏程序。图5.18 显示了多边形的剪裁。 图5.18 一个需要被剪裁的多边形 为 了实现多边形剪裁,我使用了Sutherland-Hodgeman多边形剪裁算法。关于该算法在书籍 《Principles and Practice in C (2nd Ed.)》(由James Foley,等编写)的3.14.1计算机图像部分 有讨论。 这个算法是相当简单易懂的。按照顺时针方向,将多边形的点排列好,然后考虑多边形每一对相邻的两点。如果第一个点在平面的前面(调用 点和平面位置关系函数进行判定),你应该把这个点加到被切掉的多边形的末尾,然后重新按顺时针方向排列多边形的点,可以得到如图5.19所示的排列。如果 第一个顶点和第二个顶点分处于平面的两边,那么你就要找到切割点并把它加入到列表中。虽然这样不是很直观,但是算法确实是这样做的。图5.19用图形的方 式显示了算法工作的步骤。表5.21是这种操作的代码,如果剪裁多边形成功,则函数返回TURE。 if( ( thisRes == ptBack && nextRes == ptFront ) || ( thisRes == ptFront && nextRes == ptBack ) ) { // Add the split point out->pList[out->nElem++] = Split( in.pList[thisInd], in.pList[nextInd] ); } thisInd = nextInd; thisRes = nextRes; } if( out->nElem >= 3 ) { return true; } return false; } [End] 如果你已经有了把多边形在平面后面的部分剪裁掉的代码,那么创建一个函数把被剪裁掉的部分作为一个新的多边形保存也不难。这个操作将把那些有元素在平面两侧的多边形切割成截然不同的两部分,完全在平面前面的一部分和完全在平面后面的一部分。 本章最后的BSP代码就是使用多边形剪裁。此算法直接从剪裁代码演变而来,代码非常相似。 表5.22:plane3::Split [Begin] bool plane3::Split( polygon polygon { // Make sure our pointer to the out polygon is valid assert( pFront ); // Make sure our pointer to the out polygon is valid assert( pBack ); // Make sure we're not passed a degenerate polygon assert( in.nElem>2); // Start with curr as the last vertex and next as 0. pFront->nElem = 0; pBack->nElem = 0; int thisInd=in.nElem-1; int nextInd=0; ePointLoc thisRes = TestPoint( in.pList[thisInd] ); ePointLoc nextRes; for( nextInd=0; nextInd nextRes = TestPoint( in.pList[nextInd] ); if( thisRes == ptFront ) { // Add the point to the front pFront->pList[pFront->nElem++] = in.pList[thisInd]; } if( thisRes == ptBack ) { // Add the point to the back pBack->pList[pBack->nElem++] = in.pList[thisInd]; } if( thisRes == ptCoplanar ) { // Add the point to both pFront->pList[pFront->nElem++] = in.pList[thisInd]; pBack->pList[pBack->nElem++] = in.pList[thisInd]; } if( ( thisRes == ptBack && nextRes == ptFront ) || ( thisRes == ptFront && nextRes == ptBack ) ) { // Add the split point to both point3 split = Split( in.pList[thisInd], in.pList[nextInd] ); pFront->pList[pFront->nElem++] = split; pBack->pList[pBack->nElem++] = split; } thisInd = nextInd; thisRes = nextRes; } if( pFront->nElem > 2 && pBack->nElem > 2) { // Nothing ended up degenerate return true; } return false; } [end] |