TangleTracer(Basic edition/B) 1.0 Beta开源发布

TangleTracer是一个实验性光线跟踪渲染平台,目的在于为真实感绘制技术的初学者提供完备的学习例程和友好的实验接口。系统设计思路主要源于Kevin Suffern的《Ray tracing from the Ground Up》一书,同时部分参考了著名的真实感绘制圣经《PBRT 2nd》。从今年三月中旬起我们就开始了前期的技术储备工作,该项目由四月初正式开始,截至目前完成代码近9万行,系统框架基本完善,实现并调试通过了440个demo或Kevin书中的习题(这部分代码超过4万行)。与K书中提供的原型系统不同,项目开发平台选择了Microsoft Visual Studio 2010和Qt framework 4.7.3,为快速开发提供了良好基础。 由于系统和部分框架来源于K书,我们循例采用GPLv2开源发布。此次发布的主要内容是系统的代码部分,为便于大家测试,我们稍后会提供基于win32平台的二进制版本(目前正在进行兼容性测试),运行系统内置的全部例程需要同时满足以下条件: 1.下载例程所需的纹理包:http://www.raytracegroundup.com/downloads/TextureImages.zip%EF%BC%9B 2.下载例程所需的模型包:http://www.raytracegroundup.com/downloads/PLYFiles.zip%EF%BC%9B 3.确保纹理包的TextureFiles目录和模型包的PLYFiles目录与程序二进制文件位于同一目录下; 4.部分模型需要下载stanford的原始文件并进行替换(具体请阅读README及代码注释):http://www-graphics.stanford.edu/data/3Dscanrep/%EF%BC%9B 此次发布的是TangleTracer的Basic edition/B版本,其目的在于提供思路清晰的CPU渲染技术,但实际渲染性能较低。我们正在进行系统的并行迁移实现,并力求在下半年提供TangleTracer的GPU edition/G版本。详情请关注项目github主页:http://mp77.github.com%E3%80%82

Comments

《武训传》的现实意义

我对所谓的“国产禁片”并不感冒。其原因在于真正要被禁的影片根本不可能启动拍摄,而已经拍出来的所谓“禁片”注定是褪去了核心思想的阉货,顶多被允许在课堂上当教学片放,还需得是遮遮掩掩的那种。不过,当听到《武训传》要出数码修复版时又不禁小兴奋了一下,突然对这部筹划于上世纪四十年代初、成片于1950年并在中国当代史上扮演了重要角色的“第一禁片”十分好奇起来。尽管寻找片源时颇费了一番周折,好在终于在周末第一次看完时长197分钟的全片。

使我对该片产生好奇的第一个问题是,《武训传》在今天仍不失影响力,那么是因为传主事迹确实称得上“感天动地”、堪得流芳百年?还是由于该片后来“意外地”成为了首个思想文化批判与改造运动的漩涡中心,以致于迄今为止仍有许多人们念念不忘,难以释怀?我们知道,影片创作最初源于民国著名教育家陶行知先生对“武训精神”的推崇和力行,并随段承泽和漫画家孙之俊两位先生的《武训先生画传》而广为人知(1950年李士钊和孙之俊又重新出版了一套《武训画传》)。据《武训画传》载,武训(武七)幼年丧父,与母亲以讨饭为生,年少时先后在自己的姨夫家和李地主家做工,饱受欺辱。由于没上过学,被地主骗光了三年工钱。经过几番沉重打击后,武训意识到只有读书才能改变穷孩子们的命运,才能真正帮助穷人摆脱这种不公正。经过近四十年行乞的勤劳与节俭,武训先后创办“崇贤”、“杨二庄”以及“御使巷”三家义塾,免费供贫困子弟上学。武训事迹轰动山东,最终得到清廷嘉奖并授予“义学正”。总的来说,武训事迹是真其人、确其事的贫民教育典范,理应作传以传后世。不过,当漫画版传记到银幕则产生了原因复杂到令人难以理解的后果。

昆仑公司出品的《武训传》成片于1950年,实际上影片曾断断续续拍了几年时间,周恩来等领导人都曾经对拍摄中的剧本进行了修改和建议。不过,影片的编导对原《画传》的核心思想进行了改编,我认为这恐怕是为创作团队带来日后横祸的重要原因,但也不得不佩服孙瑜导演及其他主创对待文艺创作的理性态度和勇气。影片新引入的一条主要线索是前太平军周大的经历,北伐失利的周大与武训一同在张举人家里做工,最终与武训共同逃离,不过他的选择和武训大相径庭,后者选择兴办“义学”,而周大则去做了响马,其理想是将地主“抢光”、“杀光”,有意影射清末数量庞大的农民起义运动。影片中武训的态度很清楚:“读书改变命运”,一味烧杀并不能解决问题。但在片末,农民运动达到了高潮,响马们履行诺言回来复仇,武训看着一切只能又标志性地挠了挠头,但影片没有给出事件的最后结局。批判者指责影片推崇“改良”,在某种程度上说并不完全错误,同时这也是至今影片仍存争议的原因。不过,关于所谓的“路线”争执是自百年前至今都确实存在的,只是取决于我们怎样对待这种应当仅存于学术层面的争论。

然而影片中武训的担心也未必没有道理,当狂热到丧失理性之时,原本就缺乏思考的人们会变成怎样可怕的怪兽?君不见,当代中国又横遭了多少磨难?我对影片与《画传》的一处细节差别印象深刻,首先片中强调“张举人”的读书人身份,后来又编出什么“没收武训所积造文昌帝庙,以明读书人之修身齐家的志愿”?读书人是不完美,但人始终无完人,影片着重渲染了一干“读书轻义”的当权者,与武训到处跪求贫困家庭送子入学形成了明显矛盾,尽管片中也给出了几个开明乡绅的形象。而当武训听到对“学而优则仕”的解释时又大为困惑起来,是顺带批评了当时的知识分子么?可见,影片所带给人的思考余地是非常广阔的。

现在回去看《武训传》所遭遇的不公正待遇,我认为这正是影片创作者们的担心终成现实的结果,甚至可能丝毫不出他们所预料。当然恐怕有更多的人宁愿相信这些卓越的文艺创作者们是犯了某些错误,至少是某一“特定历史时期”下的错误。但我始终觉得这些说辞只会是无力面对历史的辩解之词,看似轻描淡写,实则是一个讳疾忌医的病人罢了。

最后,针对《武训传》的当代史研究在最近30多年里已经著述颇丰,我认为要了解一段历史,必须行史学家的态度去阅读文献,在此之前不应轻下结论,否则必然是一叶障目。本文不提非官方的野史故事,理由一是资料真假难辨,有失严谨;二是希望现今的人们能继续从本质上对武训及这部影片展开讨论,当然这种讨论必须是完全理性的、有意义的观点和看法。尽管我们长久以来避而不谈,但历史终究会使我们不得不去思考,今后的路朝哪里走。

Comments

参数化设计/建模及其在艺术创作领域的应用前景

一、概述

迄今为止,参数化设计/建模仍代表一种理念,尽管这种理念已经经历了CAD/CAM近五十年的发展历程。系统化的CAD/CAM技术包含两种基本思路,一是使用二维几何原语如点、线或样条表示三维物体,如Bezier、De Casteljau以及Sutherland这些先驱们的工作;明暗处理的需求接着被提出,从而出现了早期着色模型的研究(本文将不涉及这部分内容);即使是目前最广泛使用的Nurbs,也是A.R. Forrest早在1980年就已经完成了。

另一种思路是使用体素构造表示(CSG)进行实体建模(该技术实际上也诞生于60年代初期),其基本方法是使用一组基本几何体,如球、柱体、椎体等,对其执行交、并、补等布尔运算序列,从而构建复杂三维物体,通常再使用B-Reps表示最终生成的三维模型。这种方法由于能够保持模型拓扑的欧拉示性数V-F+E,有助于满足三维物体的基本几何特征,因而得到了广泛应用。

现在看来,CAD/CAM的关键技术似乎早在30年前就已经比较完整了,那么继续该领域的研究还存在哪些意义?事实上,CAD/CAM目前仍面临许多瓶颈,这些瓶颈已经影响了目前广泛采用此类技术的汽车制造、电子和建筑等行业的进一步发展。例如面向三维模型的可编辑性,模型编辑的目的通常有两类:一是用户设计意图发生变化,需要变更现有设计;二是模型的适用条件发生变化,不得不做出调整以适应新变化:如某些工业组件所面临的不同规格需求。在参数化设计提出之前,针对三维模型的编辑主要采用直观交互式的方法进行,其复杂程度甚至超过最初的建模过程。一些行业专家分析,模型编辑的主要难点在于该过程既要保持原组件的局部特征,还要适应新的条件,同时还要确保这种编辑不会影响组件之间的相互关系;特别重要的是,在整个设计、建模、编辑的过程中要保证一定的数据精度。提高可编辑性一度成为现有CAD/CAM技术的重要研究方向。

二、参数化设计的发展和现状

参数化设计的最初目标就是解决CAD/CAM的上述问题,并取得了一定成果,特别是在建筑业。参数化设计的本质就是使用一系列参数定义三维模型的基本特征,包括基本尺寸、各组件间关系等。在一些文献中,参数化设计也被称为关系型建模或是变量化设计。在参数化设计中,建模原语通过一组参数表示,如使用位置、长度和方向参数表示空间线段,而如果仅涉及到这部分,那么参数化设计似乎与传统的交互式设计并无不同。更进一步,为了保证模型在一定条件下的可编辑性,参数化设计要求用户建立建模规则的参数化表示。如AutoDesk推出的AutoLisp语言,实际上是通过编写脚本制定规则,而把输入参数转化成三维模型的应用在目前已经十分流行了。然而对于绝大多数设计师而言,学习编程语言其实没有太多必要,同样对于一些复杂模型的构造,编程实现会真正意味着缘木求鱼。因此参数化设计需要进一步的形式化、领域化和直观化,才有可能被大多数设计师所接受。

“约束”是参数化设计的一个重要概念,不过其提出要比“参数化设计”本身还早。约束表示针对一个或一组实体的行为关系限定。实体可看作是三维模型的一个组件,实体行为则代表该组件在不同参数下表现出的状态。例如”一组线段相互平行、垂直或共线”就是一种约束描述,每条线段受相应参数控制其行为,而各线段的参数却受到“平行、垂直或共线”的关系限定。在带约束的设计和建模过程中,无论是施约束端还是受约束端,都需要得到完整的参数列表才能进行,由于约束的存在,某些参数实际上需要通过“推理”进行估计和计算,这种推理估算参数的过程在很大程度上影响了建模效率。当前应用中普遍存在两类约束:几何约束和物理或称工程约束。平行、垂直、共线、相邻、尺寸等都属于几何约束,此外包括一些物理方程、条件关系等则属于后者。这类方法需要用户在建模初期提供实体类型、空间位置和尺寸参数,然后通过一些具体方法描述实体关系,如前面所述的编程脚本。

1982年,Gossard和Light首次提出了“变量化设计”的概念。此后十年间,由于几何造型、自由曲面和实体建模技术的迅速发展,出现了愈多针对交互性及可编辑性的改进需求。大量工作开始致力于该领域中,如前面介绍的变量化编程的提出,通过编写一段程序脚本控制建模过程,这种方法实际上并不能有效解决存在的问题。而为了保证用户设计意图的准确实现,直观交互通常是必须的,因此提供领域化的参数化设计工具是较为合理的选择。

1、基于时序的约束建模,即记录用户交互式建模时产生的参数集时序变化,在修正设计时,通过随机选取并更改时序中的参数变化内容,实现参数化设计。这种方法要求模型的构建过程保证良好的层次性;

2、变量几何和变量化设计,即通过构建基于参变量的建模公式进行参数化设计,例如利用简单解析式创建二次曲面。对于复杂模型而言,其构建公式的有效性就成为实现参数化设计的基础;

3、基于规则的变量化设计,前面两种方法在实际应用中很难真正实现,出于实用的考虑,人们首先建立基本规则库,利用人工智能方法进行推理并最终得出约束条件。例如,对于两个实体而言,系统内置了有关实体位置参数的规则,通过推理即可得出实体间的相对位置关系(对于直线段可能就是平行、垂直、共线、相交或其它情况)。实际上,该方法目前仍是参数化设计的研究热点之一;

4、基于特征的参数化设计,从视觉上看,特征指模型在某些观察角度下所呈现出的特殊现象。而在参数化设计中,特征包括了与其相关的实体、参数和关系式,特征是相对于变量或规则的更高级别的语义描述,例如描述物体表面有一处“凹陷”,这就是一种特征。但是,良好的基于特征的设计需要精心设计特征库。

三、参数化设计的应用现状

尽管参数化设计目前已经深入工业设计和制造业,但最令人瞩目的则是其在建筑设计领域中的应用。应注意的是,由于建筑设计本身带有艺术气质和时代特征,引领前端的通常都是著名研究机构或设计公司,而更多的从业人员仅仅是采用稍成熟方法满足需求即可,因此参数化设计在该领域存在着两种截然不同的应用思路。

毫无疑问,最早提出的变量化编程已经为大多数CAD建筑设计师所知了,如AutoLisp。另一方面,参数化设计在这一阶段的主要工作是实现2D和3D的协同设计,具体方法可以是时序法或变量几何。在这期间许多计算机科学家努力向领域专家们推荐各种工具和应用,如William Mitchell于1987年推出的《The Art of Computer Graphics Programming. A structured introduction for Architects and Designers》,书中使用了流行的Pascal语言描述建模脚本。

在许多人看来,上述编程建立三维模型的应用过于抽象,可行性极低。事实亦如此,变量化编程目前已经成为参数化设计领域的非主流技术,而对我们来说更加常见的可能就是诸如ArchiCad或3DStudio Max:直观的交互式界面,以及大量参数输入对话框,该模式至今仍占据绝对优势。后者的缺点是,用户很难针对复杂模型整体进行参数化考虑,除非建模阶段存在非常精妙的参数设计,而这往往是不现实的。一种解决方法是确保层次化建模,例如K. Martini试图采用面向对象程序设计中的层次化类结构模拟建模过程。由此我们可以联想到一系列层次化方法,如集合、树等;另一方面,考虑到层次化建模的可行性尚未得到证明,我们还可以考虑使用图表示参数化设计中实体之间的约束关系。针对这方面的讨论目前仍是非常开放的问题。

四、艺术创作中的参数化设计思想和实践

首先,这里所说的艺术创作实际上包括前文提到的工业设计;另一方面,我们希望能在更多的艺术设计领域引入参数化的元素,其目的在于:1、大部分现有的艺术创作实际上与商业应用相关,原创效率和大规模应用能力将成为关键的风险因素;2、参数化设计为创作前所未有的艺术形式提供了新途径,而并非字面上的对艺术创作施加“约束”的意思。

达芬奇的《维特鲁威人》堪称把建筑学和人体解剖学相结合的传世名著,而这种艺术上的联系同样也反映到现代CAD/CAM技术中。香港中文大学的王昌凌(Charlie C. L. Wang)就在现代服装设计中引入参数化设计技术,并提出了一种新的基于人体特征的参数化设计方法。该方法从人体点云中提取相关特征,并在参数化模型(注意这里的参数化与“参数化设计”的区别)的基础上使用参数化设计创建新的模型。其中人体模型的参数化包含两个主要阶段,首先使用激光扫描仪获取人体点云,根据语义特征提取并重构人体的特征线框;其次,对人体网格曲面的对称信息进行建模,在特征线框上通过曲线插值生成Gregory曲面片。然后利用一种体素算法在G1连续的曲面上添加细节点云;最后再调整网格曲面的对称性。在完成人体模型参数化后,根据用户输入参数采用参数化设计方法把样例模型映射至参数化模型中,同时对样例模型的选用需依据相关策略进行。与普通的数值插值函数相比,该方法具有较低的出错率,能够保证参数化模型的正确性。基于正确的参数化模型,即可在人体上自动导入使用参数化设计方法在样例模型上设计的服装模型。

事实上,关于服装的参数化设计在最近十年已成为CAD的热点之一,并取得了一些研究成果,但仍鲜见规模化应用。而对于参数化设计技术本身而言,能否继续扩展其应用范围、并在此过程中完善自身的方法论,将成为CAD发展的一个未知数。不过,进一步扩展出有效的杀手级应用则是当务之急。

Comments

新海诚的云和他的孩子们

克查尔特把所有的记忆寄托在歌声里留下,歌声将改变形状一直流传着,在空气的振动中不断下去,不知不觉融合在我们的身体,如此在世界的某个地方永远留存下来。

[singlepic id=45 w=480 h=320 float=]

影片《追逐星星的孩子们》充满吉卜力式的风格和桥段,乍一看让人觉得影片监督和宫老有着千丝万缕的联系,然而漂浮在天空的云却在不断提醒着我们完全不同的事实。新海诚把英文片名定为《Children who Chase Lost Voices from Deep Below》,相比于日语片名恐怕更贴近故事情节,而就后者来说,来自雅戈泰的迦南族人瞬恐怕是唯一真正“追逐星星”的孩子,其余的主角么,当然是为了寻找位于菲尼斯·特拉生死门另一端的灵魂了。本片正是一部讴歌生命的唯美之作。

事实上,这个星球上的每一个人在新海诚眼里都是影片所讲述的孩子,自出世起就受到了来自雅戈泰神明的诅咒,从而死亡就成为生命必须的部分,灵魂最终被融入雅思特拉里。然而总会有许多人无法真正面对死亡,甚至希望故人能够得以重生。生命之船谢库纳·威玛纳的回应是要求一个完整的肉身以及一只许愿人的眼睛——这恰恰体现出一种绝对公平,所谓的复活也根本毫无意义。人类所需要做的,即是背负着愈来愈多的离别与思念度过一生,也是女主角明日菜最终意识到自己执意进入雅戈泰的原因:生活除了日复一日的单调重复,其余的就是想念了。毫无疑问的是,这次生命之旅让女主角卸下了曾经的包袱,从此更加乐观地面对生活。付出沉重代价并再次失去理莎的森崎老师也逐渐走出了十余年前亡妻的阴影。心也彻底摆脱了失去哥哥瞬的心结。

而对于另一个男主角瞬而言,他原本就属于跟随克查尔特在数万年前进入地下的人族后代,尽管雅戈泰人都对生命的时间线充满敬畏,而在瞬的心里,看一眼地上世界夜空中的繁星,以及那个潜意识中似曾相识的人,已是他在临死前最后的愿望。而当这种情绪达到最强烈时,歌薇丝感应到了另一个世界中的心灵,从而带瞬来到了这个世界,在临死前对明日菜进行了生命的祝福,故事也就此展开。令人感到奇怪的是,尽管瞬坦然接受了死亡结局,但依然在命运中破除了雅戈泰人的世代约定:即永不允许地上人进入这个世界,而是选择追求自由——这也是人类无止境地追逐繁星的真正意识。这恐怕是新海诚对生命从另一个角度的诠释。

至于片头提及的地上人的堕落,故事并没有在此展开讨论,恐怕是脚本原始设定上的一个小失误。然而《追逐星星的孩子们》给人以强烈的宫崎骏式的感官,同时又夹带了《EVA》的世界观(威达之水 vs LCL?),这让许多对新海诚充满期待的资深动漫迷们感到无法接受。但是无论如何,观影的过程是让人感到无比舒适的。

附惊艳片尾曲歌词《Hello Goodbye & Hello》:

Hello Goodbye & Hello

相遇 分别与重逢

词曲:熊木杏里

编曲:清水俊也

歌:熊木杏里

君(きみ)に会(あ)って

今(いま) 君(きみ)とさよなら

Hello goodbye and hello

そして君(きみ)のいない

この世界(せかい)にHello

本当(ほんとう)のさよならを

知(し)らなかったあの时(とき)

壊(こわ)れゆく心(こころ)はずっと

君(きみ)を探(さが)してた

もしも届(とど)くのならば

伝(つた)えたかったことがたくさんある

すべての気持(きも)ちで

君(きみ)の笑(え)み颜(かを)を

绝(た)やさずそばにいたいと誓(ちか)うよ

Hello goodbye and hello

君(きみ)に会(あ)って

今(いま) 君(きみ)にさよなら

Hello goodbye and hello

そして君(きみ)のいない

この世界(せかい)にHello

思(おも)い出(で)が温(ぬく)もりは

君(きみ)へと続(つづ)く糸(いと)

たとえても见(み)つからない

それだけを见(み)つけた

失(な)くしたくない愿(ねが)い

一番(いちばん)

远(とお)い星(ほし)だと思(おも)ったよ

空(そら)は広(ひろ)がる

明日(あす)のように

果(は)てないけれど手(て)を伸(の)ばしたいよ

Hello goodbye and hello

君(きみ)の事(こと)を

いつも忘(わす)れないよ

Hello goodbye and hello

そしてこの道(みち)を

歩(ある)いてゆくなあ

君(きみ)を好(す)きになった时(とき)から

始(はじ)まていたこの旅(たび)…

Hello goodbye and hello

君(きみ)に会(あ)って

今(いま) 君(きみ)とさよなら

Hello goodbye and hello

そして君(きみ)のいない

この世界(せかい)にHello

Hello goodbye and hello

君(きみ)に会(あ)って

今(いま) 君(きみ)にさよなら

Hello goodbye and hello

そして君(きみ)のいない

この世界(せかい)にHello

Comments

有关Beta 2和Kinect for Windows商用前瞻

在本月初Kinect正式发布一周年之际,微软公司副总裁Frank X. Shaw在公司官方博客上发表纪念文章“Feeling the Kinect Effect”,并透露Kinect的商用计划“Kinect for Windows commercial program”,该项目将于2012年初正式公布。文章称专门负责商用计划的团队已获得来自超过25个国家的知名公司共超过200项商业应用申请,这些应用覆盖了20多个不同领域,这恐怕会令即将到来的2012年成为名副其实的“Kinect年”。

而在上个月27号,微软注册了kinectforwindows.org域名并将其作为Kinect for Windows项目新的官方网站,紧接着Kinect for Windows SDK 1.0 Beta 2在本月5号正式发布。相较于8月份后就再没更新过的Beta 1,新版本SDK在以下功能上进行了持续改进:

1、骨骼跟踪功能得到显著增强,在跟踪精度、计算和传输性能上有了明显提升。此外骨骼跟踪现在能够正确支持多核处理器,在同时使用2个Kinect时,开发人员也可以指定专门用于骨骼跟踪的设备了;

2、API现在能够对设备的状态进行有效检测和管理了,例如device unplugged, device plugged in, power unplugged等,这样应用程序就能在系统从待机状态恢复时自动重新连接设备了。一个很好的程序示例被写进了新版本的Shape Game Demo里;

3、在WPF程序中使用语音功能的开发人员不需要从额外的线程访问DMO了,现在可以直接从UI线程创建KinectAudioSource,从而能够简化现有工程的代码。

4、新的驱动程序、运行时系统和SDK现在能够在Windows 8 Developer Preview中使用了。

5、现在能直接创建64位应用程序;

6、NuiImageBuffer被新的INuiFrameTexture代替,新定义在MSR_NuiImageCamera.h中,现有项目不再需要引用NuiImageBuffer.h了;

7、安装目录的结构进行了调整,现在的安装路径使用环境变量%KINECTSDK_DIR%定义,默认值为C:\Program Files\Microsoft SDKs\Kinect\v1.0 Beta2;

8、示例代码的更改包括:

一个新的C#程序示例:KinectAudioDemo;

示例程序现在被默认安装在C:\Program Files\Microsoft SDKs\Kinect\v1.0 Beta2\Samples,你需要Unzip以便查看源代码,而这里建议将源码Unzip到Program Files以外的目录去;

9、驱动程序和运行时系统的稳定性和性能进一步提升,尤其是在托管API层

总的来看,Beta 2并未提供关键功能上的改进,更多是对现有问题的修正。此外,备受瞩目的Kinect for Windows杀手级应用仍未出现,社区反馈是目前很多未公开的商业项目在尝试OpenNI,对MS SDK反倒比较谨慎。不过从长远角度考虑,MS SDK在开发社区、系统平台和资源支撑方面拥有众多优势,明年商业化后必然会率先占据有利位置。由此联想到近期在4S上被热炒的Siri,看来自然交互的春天真的离我们不远了。

Comments

从SIGGRAPH’11看《Rango》

一年前,我们曾介绍并讨论了展示在siggraph’10 Talks上、作为Disney’s 50th Feature Animation的《Tangled》。在今年8月7日至11日的温哥华siggraph’11大会上,工业光魔携首部动画长篇《Rango》成为了“Let there be light”(加点儿灯光?)单元当之无愧的主角(尽管去年的Tangled和今年的Cars 2同时均有入选)。

《Rango》中文译名“兰戈”,由执导了前三部加勒比海盗系列的Gore Verbinski担任导演,该片作为工业光魔在Feature Animation(FA)领域的处女之作,使得后者在首次试水FA后即登上了巅峰。影片主要讲述了一只不慎被遗失在西部荒漠中的蜥蜴,历经艰难、最终找回自我的故事。影片无论在情节构思、画面、配乐上的精细程度都堪称前所未有。本文依旧从Siggraph’11上的三篇文章出发,试图探索堪称计算机图形学史上迄今最先进的工业级CG技术。

使用面向FX工具制作动画角色和光照特效

在拍摄初期,工作人员在虚拟转台上测试了整个ILM的材质库,其意在测试材质的环境光遮蔽和阴影效果。然而在影片中,除了60名主要角色以外,还需要大约1000个道具。为了简化这部分工作,这里还使用了部分置换贴图以丰富材质信息。同时,再将这些材质用于不同环境的转台程序下,以便迅速发现问题并制作新的Shader;

过去通常使用光板或参考集设置光线,然而《Rango》则例外,这部分影片中引入了新的方法。每一组镜头中的光照设置非常简单,即包括关键光、环境光、反射光三部分,该方法被用于全部的室外光和一些室内光。技术人员使用Digital Matte部门开发的虚拟集渲染器创建环境光和反射光映射,而非直接进行360度捕获。上述映射被用于非静止的、处于快速移动物体中的反射/折射光精确表示。设置灯光时,关键光通过一个受可切变天空顶约束的远程光Shader定义,并通过复制角色的远程关键光,达到较好的交互和阴影效果。同时,用户还可以通过旋转整个装置以达到尽可能广泛的变化,或只旋转关键光以改变复制光,或通过特定的扭曲修改角色局部的复制光。

除个别场景外,影片几乎所有的室外场景都使用了前文所述的格式化远程关键光、环境光和反射光装置。该装置包含两个开关:一是激活阳光反射卡片(模拟光晕效果),另一个用于开启基于距离的阴影模糊。

假设你在一个阳光明媚的室外场所,挥动手臂并在一张白纸上映上阴影效果,你会发现当你升起手臂时,阴影边界的模糊度随之逐级递增,这种边界阴影模糊在物理学中被称为“半影”现象。物理学中已经证明,半影宽度除以障碍物至阴影面的距离值约为常数1/112,它同时也是太阳以弧度为单位角直径值。工作人员实现了基于距离的关键光阴影边界模糊并对其进行严格控制,力求在符合真实世界效果的基础上尽可能满足导演对夸张手法的要求。

简易的关键光/环境光设置方法是设置全局光照参数,并将其作为一组镜头的初始光照。整部影片也可以通过该方法初始化并检测通用关键光的特效和镜头效果。每当TD(Technical Directors)布置新任务时,他们就可以通过添加特定的光照效果以丰富镜头。出于对主角显著区别于背景的绘制需要,工作人员对其进行了逐帧细化和调整。

角色合成方面起初的构想十分简单,然而随后愈加复杂,这种复杂程度甚至超过了真实的角色电影。导演对每一个场景均有特定的灯光、编排和镜头要求,还包括变化的气候、照片级真实感绘制(光畸变,正确的燃烧扭曲,热涟波和相机抖动等)。额外还包括了沙漠、指纹、水、尘土等各种细节效果。为此制作团队特组织了一批强大的计算机开发团队,通过创建自定义通用脚本制作了多个工具辅助影片拍摄。

动物皮毛的穿透避免和动力学

在《Tango》中需要对多个鸟类角色建立羽毛模型,而这种羽毛模型通常需要手动放置主要的羽毛茎秆曲线,再在不允许插值的条件下实例化整个皮毛几何体。此外,影片对动物角色的要求还包括了日常姿态、呼吸动作、受风、动量、惯性影响的效果等。这里制作团队开发了一种尽可能保持原设计形态的皮毛动力学模拟和自由碰撞技术。

鸟类的羽毛是通过分层有序叠加的,开发人员应允许设计师能够任意设计皮毛的基本外形、层次结构,且不必处心积虑地避免皮毛穿透问题。设计师通过在角色皮肤上放置引导曲线,以构建基本的皮毛外观,同时在其径向上实例化一个矩形几何框,用户可据此设置宽度、扭曲度、与起始点法线相似度等参数。该四边形面片同时还被用于在绘制阶段实现多种纹理效果。

制作团队共使用了两个实例化的皮毛集合,一是目标集,包括了许多皮毛间的相互穿透效果。二是特征皮毛根集,这部分使用狭长的面片实现,由于其更接近法线方向,故存在较少的互穿透。后者用于对某些动物的皮毛进行初始化和带穿透避免的布料模拟。与[Weber and Gornowicz 2009]提出的复杂方法不同,下一步是使用非身体力设置布料模拟的皮毛。每一个布片的基被限制在一个曲面片上,据此检测面片间的相互碰撞。目标集则是通过布料解析器限制下点接点的受力建立。在模拟过程中,布料面片将面向目标集方向生长、然后由碰撞检测部分避免穿透。碰撞避免的方法可以使用基于三角形或是基于边的排斥方法。在第一步完成后,仍需要进一步对角色动作过程中的皮毛穿透现象进行检测, 同时还要保证对原设计形态的维持。

插值毛发的碰撞精度

毛发的动力学常常是通过引导毛发和成员插值构成的,因此这部分也被称为插值毛发。碰撞精度是毛发模拟技术中最费时的阶段之一,因此通过限制毛发的碰撞精度从而提升动力学性能是非常重要的。在许多情况下,只需要视觉感官上可被接受即可不需增加碰撞精度,遗憾的是,我们的运气往往没有这么好:)

影片中许多动物都穿有衣服,那么即使是一根发丝从衣服布料中伸出来都是不可接受的。然而对整个已生成的毛发集合进行穿透检测和避免也是不现实的:首先,这种技术非常耗时,插值后的毛发往往是引导毛发的成百上千倍;其次,这种方法并不能用于目前的管线设置,因为在模拟阶段插值头发尚不存在;最后,基于物理的碰撞检测方法通常是使用多边形面片,而非Catmull-Clark细分曲面表示,即使是极小的曲面差异也很难保证碰撞检测的正确性。

这里制作团队提出了一个新的穿透修正方法,该方法主要解决的问题包括精度、效率和时间相关性,其主要贡献是对平滑曲面表示进行碰撞检测,沿着毛发生长方向进行连续碰撞检测,并在每一帧进行碰撞检测。该方法最终在影片多处被用到,它既保证了高效率,同时精度也达到了无需进行后期修正的程度。

文中工作人员使用了[Verma et al. 2009]将CC曲面转换为Bezier表示,这种显式曲面允许通过精确交叉查询解决碰撞检测的问题。其次,使用层次数据结构对毛发和Bezier面片进行候选碰撞检测。由于当前配对可能发生了穿透,将毛发的生长方向再移动至其参数空间中的上层部分。通过沿参数化的曲线空间搜索连续毛发-曲面交叉,其中使用Newton法计算毛发和曲面交叉部分。

在穿透避免时很容易造成无法保持原始形态的问题,这里使用了一种简便方法以保持形态,即在穿透避免后使用[Goldenthal et al. 2007]提出的快速投影法修正其差异长度,然后在碰撞精度和应用不可延展间进行迭代,直到满足两方面需求即停止。实际应用中只需要很少的迭代次数即可完成计算。

插值头发绘制时逐步生成的,同时还要同步进行穿透避免计算。因此需要在每一帧进行独立的穿透避免计算(并行处理),这条限制有一定隐患,即可能导致发生时间无干性:毛发可能在连续帧中发生不平滑。这里通过尽量使其朝向生长方向以降低不平滑的程度,尽管尚不完美,但已经足够达到目的了。

总结

《Rango》无疑是siggraph’11大会上一颗耀眼的明珠。与去年的《Tangled》相比,该片的制作成本要少一亿三千万美元(估计成本一亿三千五百万),前者更是花费了Disney Animation Studio长达六年时间。不过,这次ILM的口碑和舆评显然超过了Disney,正如片尾Rango骑着白鸡,发表了如下慷慨激昂的讲话:

My fellow comrades.

There will be times when you doubt yourself.

When you feel pummeled by the cataclysms of life,

Remember this moment. Remember me.

Know that I will be there watching you,

Sometimes at inappropriate moments.

That’s part of the deal.

And remember, within all of us resides

The true spirit of the…

Comments

KINECT来了——解析SDK(MS SDK 2)

NUI图像数据流概述

NUI的流数据是通过连续静态图像序列传递的。在上下文初始化阶段,应用程序将识别需要读取的流数据,并对其进行附加的流相关设置,包括数据解析度、图像类型、用于存储输入帧的缓冲区数量等内容。在应用程序检索并释放相关帧之前,如果运行时数据占满了缓冲区,那么系统将自动丢弃最旧的帧并重用缓冲区,也就是说,帧数据是可被丢弃的。同时系统最多允许请求四个缓冲区,而在大多数应用情形下通常只需要其中的两个。应用程序可通过API获取如下类型的图像信息:彩色图像数据、深度图像数据、用户分割数据等。下面将分别对上述三种类型的数据进行一些说明。

彩色图像数据:系统提供两种格式的彩色图像数据,包括32位X8R8G8B8格式的sRGB位图数据和16位的UYVY格式的YUV位图数据。由于两者实际来自同一图像数据,因此两种格式的最终图像实际上没有任何差别。不过后者要求图像保持640x480的分辨率和15FPS的帧率,同时内存需求更小。应注意,由于系统采用USB连接,传感器层会首先将1280x1024分辨率的Bayer彩色滤波马赛克图像压缩并转换为RGB格式传输,运行时系统再对该数据进行解压缩操作。上述特性可以保证数据帧率可达30FPS,但解码操作会损失一定的图像精度。

深度图像数据:深度图像帧中的每一个像素点都表示从摄像头所在平面到视野内最近物体的笛卡尔坐标系距离,单位为毫米。系统目前支持630x480、320x240、80x60三种规格的深度图像帧。应用程序可根据深度图像数据跟踪人物动作、识别并忽略背景物体。深度图像数据含有两种格式,其一是唯一表示深度值:那么像素的低12位表示一个深度值,高4位未使用;其二是既表示深度值又含有人物序号,则低三位保存人物序号,其余数据位表示深度值。应注意如果获取的深度值为0,则说明物体距离摄像头过近或过远以致超出了设备规格。

用户分割数据:SDK beta目前支持读取两个用户的分割映射数据,其数据帧的相关像素分别记录了用户的序号。尽管用户分割数据是独立生成的数据流,在实际应用中仍可以将深度数据和用户分割数据整合成一个帧,其中像素值的高13位保存了深度值,低三位保存用户序号,其中序号为0则表示无用户,1和2分别表示两个不同的用户。在实际应用中,应用程序往往利用用户分割数据在深度图像和原始彩色图像中获取ROI感兴趣信息。

基本编程模型

基于NUI API的编程模型本质上就是获取传感器图像数据的过程。应用程序往往通过相关代码首先将图像的最后一帧读入至缓冲区中,如果该帧已预备好,那么其将进入缓冲区,如果帧数据尚未就绪,代码仍可选择是否继续挂起等待或暂时释放并稍后重试。NUI摄像头API绝对不会多次传递相同的图像数据。框架包含的基本编程模型如下:

1、POLLing模型,该模型较为基础易用。首先应开启图像数据流,然后请求并设置等待下一帧的时间,范围允许从0到无穷大,单位为毫秒;如果帧数据尚未就绪,则系统将等待刚才指定的时间然后返回。如果帧数据成功返回,则应用程序可请求下一帧数据并在同一线程执行其它操作。通常一个C++应用程序应调用NuiImageStreamOpen函数首先启动一个彩色或深度数据流,并忽略可选事件。托管代码则需调用ImageStream.Open方法。请求彩色或深度图像帧的C++函数为NuiImageStreamGetNextFrame,C#为ImageStream.GetNextFrame方法。

2、事件模型,事件模型允许将获取骨骼帧的功能精确、灵活地集成入应用程序引擎。在该模型中,C++程序首先调用NuiImageStreamOpen函数并传入一个事件句柄。每当一个新的图像帧数据可用时,事件信号将被触发。任何相关的等待线程将被唤醒并通过调用NuiImageGetNextFrame函数获取骨骼信息,与此同时事件将被系统重置。托管代码应绑定Runtime.DepthFrameReady和Runtime.ImageFrameReady事件到相关的处理函数,当新数据可用时,处理函数可调用ImageStream.GetNextFrame获取该数据。

NUI骨骼跟踪应用

NUI还包括一个Skeleton骨骼跟踪模块,该部分提供最多两名用户的详细位置和朝向信息。骨骼跟踪的输出是一个点集,称作Skeleton Positions,该点集表示了一个完整的人体骨骼信息,如下图所示。

[singlepic id=44 w=320 h=240 mode=watermark float=center]

骨骼位置信息表示了用户当前的位置和姿态,如果要使用骨骼跟踪功能,应用程序应在NUI初始化阶段设置相关内容。NUI骨骼数据获取的方式与NUI Image部分基本一致,其基本编程模型也包括Polling和Event两种。前者的C++函数为NuiSkeletonGetNextFrame,托管函数为SkeletonEngine.GetNextFrame;后者的C++句柄绑定操作由NuiSkeletonTrackingEnable完成,并在处理线程内调用NuiSkeletonGetNextFrame;C#则使用Runtime.SkeletonFrameReady绑定事件,然后调用SkeletonEngine.GetNextFrame获取相关信息。

骨骼跟踪模块通过深度数据计算地板裁切面,其基本方法将在后文进行介绍。如果应用程序在NUI初始化时开启了骨骼跟踪功能,则其每处理完一套深度数据则就放出相应的骨骼数据信号,而无论该深度数据中是否真的包含骨骼信息。应用程序使用地板裁切面数据获取骨骼框架,返回的骨架信息将携带一个时间戳以和相关的深度信息进行匹配。

NUI骨骼跟踪分主动和被动两种模式,提供最多两副完整的骨骼跟踪数据。主动模式下需要调用相关帧读取函数获得用户骨骼数据,而被动模式下还支持额外最多四人的骨骼跟踪,但是在该模式下仅包含了用户的位置信息。对于所有获取的骨骼数据,其至少包含以下信息:

1、相关骨骼的跟踪状态,被动模式时仅包括位置数据,主动模式包括完整的骨骼数据。

2、唯一的骨骼跟踪ID,用于分配给视野中的每个用户。

3、用户质心位置,该值仅在被动模式下可用。

4、对于主动模式下的骨骼跟踪数据,还包括用户完整的骨骼数据。

5、对于被动模式下的骨骼跟踪数据,仅包括用户位置信息,不包括详细的骨骼数据。

NUI坐标变换原理

深度图像空间:这是一个仅包含物体到传感器法平面的深度数据的投影空间,其z值是唯一有效的,而x、y值仅仅是进行了插值计算的结果,其数据实际并无任何物理意义;

骨骼空间:用户骨骼位置使用x、y、z三维坐标表示,与深度图像空间坐标系不同的是,该空间的单位为米,且是一个传感器朝向为z轴正向的右手系。应注意,骨骼空间坐标系与Kinect位置息息相关,如果传感器被放置在一个非水平面上,那么计算得到的坐标系可能并非是标准形式,最终的用户数据也有可能是倾斜的。

地板裁剪面的确定:骨骼数据均需要包含一个地板裁剪面向量,该向量保存了与地板平面方程有关的系数coefficients,骨骼跟踪模块通过地板裁剪平面除去背景,并将用户图像分割出来。对于平面的一般式方程Ax+By+Cz+D=0,该方程经过规范化即D值实际是指传感器到地板平面的高度值。如果地板不可见,那么地板裁剪平面向量实际为0。地板裁剪面向量值可在NUI_SKELETON_FRAME结构的vFloorClipPlane成员中获取。托管代码则保存在SkeletonFrame.FloorClipPlane域中。

骨骼镜像:默认的用户图像实际上是一个镜像数据,也就是说该数据表示了用户当前面朝屏幕内部。然而有时确实需要图像面向用户本人一侧,那么需要确定一个镜像变换矩阵以达到此类要求。

Comments

KINECT来了——解析SDK(MS SDK 1)

在8月初的文章里,我们曾对Microsoft推出的官方Kinect SDK beta进行了初步介绍,本文将在此基础上对其做出进一步说明。值得注意的是对托管代码和非托管代码的选择,从社区反馈看来似乎前者占据了绝对上风,coding4fun推荐的项目也基本都是以WPF编程为主。不过考虑到平台的潜在可扩展,这里还是推荐使用标准C++构建非托管程序。下面先给出两种编程方式在VS2010中的配置方法:

对于托管C#程序,添加Add Reference中.Net标签页的Microsoft.Research.Kinect.dll,使用using Microsoft.Research.Kinect.Nui引用NUI API的声明文件,如果还要引用音频API,还要加上using Microsoft.Research.Kinect.Audio。

对于标准C++程序,代码中必须加入<windows.h>头文件,其中NUI API需要包含MSR_NuiApi.h头文件、音频API需要包含MSRKinectAudio.h。另外需要添加相应的静态链接库,同时确保安装的DLL文件存放在PATH路径中。这里给出SDK所包含的头文件说明:

MSR_NuiApi.h,该文件包含所有关于NUI API声明,包括初始化调用和访问函数,如NuiInitialize、NuiShutdown、MSR_NuiXxx以及INuiInstance,其主要功能是枚举设备并对其进行访问调用。MSR_NuiImageCamera.h,该文件包含对NUI图像和摄像头功能API的声明,其一般形式为NuiCameraXxx和NuiImageXxx:此类API的功能为调整摄像头倾角和仰角,并启动数据流并读入图像帧。MSR_NuiProps.h,该文件包含对NUI属性枚举API函数的声明。MSR_NuiSkeleton.h,该文件包括NUI骨架API函数的声明,其一般形式为NuiSkeletonXxx and NuiTransformXxx:功能为打开/关闭骨架跟踪、获取骨架数据、将骨架数据进行变换以实现平滑绘制。MSRKinectAudio.h,该文件包括对音频API函数的声明,其中ISoundSourceLocalizer接口可返回波束方向和音源位置。NuiImageBuffer.h,该文件定义了一个帧缓冲器,其作用类似DirectX9的纹理缓冲。

Kinect for Windows应用程序架构

[singlepic id=43 w=320 h=240 mode=watermark float=center]

上图给出了beta SDK的整体组件架构,硬件层我们已经在上月进行了详细介绍。驱动的内核模式包含了设备驱动程序,上层的数据交互统一使用WinUSB数据栈,其中设备栈主要用于设备的配置和访问,摄像头栈用于视频数据流控制,USBAudio栈用于音频数据流控制;用户模式为API提供了访问和控制接口。应用层API上包括了三部分组件,其中MS SDK beta直接提供了NUI API即基本API集和KinectAudio DMO,后者的功能主要是提供波束成形和音源定位功能。此外,如果要使用Kinect的所有功能,还需要自己准备Windows 7 SDK中的音频、语音、媒体API集以及微软语音识别SDK,上述组件并未包含在SDK中。近两篇文章会主要介绍NUI API部分的设计原理和编程说明。

NUI API概述

NUI API是Kinect SDK的核心组成部分,其主要功能包括:提供连接至PC的Kinect传感器元件的访问接口、提供对由Kinect成像传感器生成的图像和深度数据流访问接口、通过经处理的图像和深度数据实现骨骼跟踪。下面我们分别使用托管和非托管代码介绍整个Kinect API上下文环境的创建和销毁。

与OpenNI不同的是,Kinect SDK允许方便地同时使用多台Kinect设备,但是每台设备(或传感器)同一时间只能被一个程序实例使用。下面是使用C++代码实现传感器枚举和访问的整个过程:

情形一:应用程序同一时间仅使用一台Kinect设备:

1、调用NuiInitialize,该函数首先会初始化一个Kinect传感器设备实例;

2、调用NuiXxx,该函数集的功能是传输图像和骨骼数据流,并管理并配置相关摄像头;

3、调用NuiShutdown结束。

情形二:应用程序同一时间使用多台Kinect设备:

1、调用MSR_NuiDeviceCount,查看可用Kinect设备的数量;

2、调用MSR_NuiCreateInstanceByIndex创建指定设备索引的实例,该函数将返回该实例的INuiInstance接口指针;

3、调用INuiInstance::NuiInitialize,该函数用于初始化针对该实例的NUI API上下文;

4、调用针对INuiInstance接口的其它方法,用于传输图像和骨骼数据流并管理相关摄像头;

5、调用INuiInstance::NuiShutdown,销毁某个设备实例的NUI API上下文;

6、调用MSR_NuiDestroyInstance函数销毁该实例;

接下来是使用C#代码实现传感器枚举和访问的整个过程:

情形一:应用程序同一时间仅使用一台Kinect设备:

1、创建一个新的运行时对象,将其参数列表留空,例如nui = new Runtime(),调用该构造函数表示在系统中创建了一个Kinect传感器设备实例;

2、调用Runtime.Initialize函数初始化NUI API上下文;

3、调用其它托管接口,用于传输图像和骨骼数据流,并管理和配置相关摄像头;

4、调用Runtime.Shutdown销毁实例。

情形二:应用程序同一时间使用多台Kinect设备:

1、调用MSR_NuiDeviceCount函数获取可用设备数量;

2、创建一个新的运行时对象,参数为设备索引号,如nui = new Runtime(index),调用该构造函数表示创建了针对某个Kinect传感器的系统实例;

3、调用Runtime.Initialize函数初始化NUI API上下文;

4、调用其它托管接口方法,用于传输图像和深度数据、并管理和配置相关摄像头;

5、调用Runtime.Shutdown函数销毁实例。

NUI API的数据处理是通过一个多级管线实现的,在初始化阶段,应用程序需要指定相应级别的子系统,这样运行时系统才能系统所需的部分管线。在初始化时系统通常允许进行以下配置:

彩色:应用程序指定从设备中获取彩色图像数据流;

深度:应用程序指定从设备中获取深度图像数据流;

深度和人物索引:应用程序指定从设备中获取深度数据流,并请求骨骼跟踪引擎当前生成的人物索引;

骨骼:获取骨骼位置数据;

上述设置指定了有效数据流类型和解析度,例如当一个应用程序没有在NUI API初始化阶段指定深度数据时,它随后也无法开启一个深度数据流。

Comments

Kinect来了——解析SDK(OpenNI Framework 4)

配置工作节点的一般方法

前文的例子中已经说明了在程序中动态配置节点信息的一般过程,节点配置完成的主要标志一般是对xn::Generator::StartGenerating()函数的调用。除此之外,OpenNI还提供了基于外置XML文件的节点配置方法,使用XML配置文件能够显著降低代码复杂度,同时提高应用程序的可用性和灵活性,本文将主要讨论上述方式结合OpenNI编程。

OpenNI支持的XML脚本功能非常强大,仅使用外置的文件就能够实现节点的创建、配置,甚至操作上下文属性(例如增加相应的许可证等)。通常可以使用xn::Context::RunXmlScript()直接执行脚本字符串,或通过xn::Context::RunXmlScriptFromFile()调用外部文件。

一个XML内容中必须包含一个OpenNI结点,该结点内共包含三个子结点:Licenses,Log和Production Nodes。其中Licenses结点提供了系统需要的额外的许可证,其格式一般如下:

[c]<Licenses> <License vendor="vendor1" key="key1"/> <License vendor="vendor2" key="key2"/> </Licenses>[/c]

Log结点用于配置OpenNI的日志系统,该结点内可添加下列可选元素属性:writeToConsole,设置日志信息是否在控制台中显示,默认为false。writeToFile:,设置日志信息是否将被写入文件,该文件将被放置在工作目录下,默认为false。writeLineInfo,设置是否每一条日志记录都需要包含文件名和行信息,默认为true。此外,Log还包含三个子结点:LogLevel,携带value属性,其值为0 (verbose), 1 (info), 2 (warnings) or 3 (errors),其中3为默认值,该结点决定了日志记录的粒度。Masks,包含一个mask元素列表,设置相应mask的开启和关闭。Dumps,包含一个dump元素列表,设置相应dump的开启和关闭。下面的XML脚本演示了一段日志配置信息:

[c]<Log writeToConsole="false" writeToFile="false" writeLineInfo="true"> <LogLevel value="3"/> <Masks> <Mask name="ALL" on="true" /> </Masks> <Dumps> <Dump name="SensorTimestamps" on="false" /> </Dumps> </Log>[/c]

ProductionNodes结点包含了工作结点的创建和配置信息,它包括了若干个子结点: GlobalMirror,设置Global Mirror属性的开启和关闭,相当于调用xn::Context::SetGlobalMirror()函数; Recording,设置是否启动一个记录,其file属性包括了记录文件名; Node,该子结点可以包含多个,它将引导OpenNI枚举并创建一个工作结点,其作用类似xn::Context::CreateAnyProductionTree()函数。其type属性表示枚举类型,值可包括下列内容:Device (XN_NODE_TYPE_DEVICE)

Depth (XN_NODE_TYPE_DEPTH)

Image (XN_NODE_TYPE_IMAGE)

IR (XN_NODE_TYPE_IR)

Audio (XN_NODE_TYPE_AUDIO)

Gesture (XN_NODE_TYPE_GESTURE)

User (XN_NODE_TYPE_USER)

Scene (XN_NODE_TYPE_SCENE)

Hands (XN_NODE_TYPE_HANDS)

Recorder (XN_NODE_TYPE_RECORDER)

Node结点的name属性允许设置一个工作节点名称。

Query,Node还含有一个Query子结点,用于枚举时的查询操作。Query可包含下列子结点:”Vendor”、”Name”、”MinVersion”、”MaxVersion”、”Capabilities”、”MapOutputModes”、”MinUserPositions”、”NeededNodes”,上述结点均可设置不同的子结点和属性,用于查询条件的选择,如果有多个子结点同时出现,那么OpenNI将按照AND进行逻辑整合。下列XML脚本演示了创建一个自定义属性的深度生成器:

[c]<Node type="Depth" name="MyDepth"> <Query> <Vendor>vendor1</Vendor> <Name>name1</Name> <MinVersion>1.0.0.0</MinVersion> <MaxVersion>3.1.0.5</MaxVersion> <Capabilities> <Capability>UserPosition</Capability> <Capability>Mirror</Capability> </Capabilities> <MapOutputModes> <MapOutputMode xRes="640" yRes="480" FPS="30"/> </MapOutputModes> <MinUserPositions>2</MinUserPositions> <NeededNodes> <Node>MyDevice</Node></NeededNodes> </Query> </Node>[/c]

Configuration子结点实际上代表了对工作节点的动态配置,其指令内容将是顺序执行的。同时,该结点几乎对应了OpenNI所有的Set配置操作,下面的例子演示了分别创建图像、深度和音频节点的过程:

[c]<ProductionNodes> <Node type="Image"> <Query> <MapOutputModes> <MapOutputMode xRes="320" yRes="240" FPS="60"/> </MapOutputModes> <Capabilities> <Capability>Cropping</Capability> <Capability>Mirror</Capability> </Capabilities> </Query> <Configuration> <MapOutputMode xRes="320" yRes="240" FPS="60"/> <PixelFormat>RGB24</PixelFormat> <Cropping enabled="true" xOffset="28" yOffset="28" xSize="200" ySize="160" /> <Mirror on="true" /> </Configuration> </Node> <Node type="Depth"> <Query> <Vendor>VendorX</Vendor> <MapOutputModes> <MapOutputMode xRes="640" yRes="480" FPS="30"/> </MapOutputModes><Capabilities> <Capability>UserPosition</Capability> </Capabilities> </Query> <Configuration> <MapOutputMode xRes="640" yRes="480" FPS="30"/> <UserPosition index="0"> <Min x="128" y="128" z="500"/> <Max x="600" y="400" z="2000"/> </UserPosition> <Property type="int" name="VendorXDummyProp" value="3" /> </Configuration> </Node> <Node type="Audio"> <Configuration> <WaveOutputMode sampleRate="44100" bitsPerSample="16" channels="2" /> </Configuration> </Node> </ProductionNodes>[/c]

通常在上述配置信息载入后,应手动调用xn::Context::StartGeneratingAll()函数启动数据流。然而也可以在XML中加入默认的Start信息,即在ProductionNodes和其Node子结点中均包含有startGenerating属性,其中如果前者的该属性为true,那么将意味着执行StartGeneratingAll(),否则只执行相应为true的工作节点。

Comments

Kinect来了——解析SDK(OpenNI Framework 3)

基本编程方法

本节的主要内容是结合OpenNI编程。

首先假设使用MS VS作为开发工具,那么应先在工程中配置OpenNI库:其中分别需要添加环境变量$(OPEN_NI_INCLUDE)和$(OPEN_NI_LIB),以及静态链接库OpenNI.lib;此外代码文件中还要添加相应的头文件,C语言工程需包含XnOpenNI.h,C++工程为XnCppWrapper.h。

下面的代码段演示了如何初始化一个上下文对象,并从深度节点中创建和读取数据:

[c]XnStatus nRetVal = XN_STATUS_OK; xn::Context context; //声明上下文对象 nRetVal = context.Init(); //初始化并返回异常编码 xn::DepthGenerator depth; //声明深度生成器节点

nRetVal = depth.Create(context); //初始化节点,返回异常编码 nRetVal = context.StartGeneratingAll(); //启动深度节点数据生成 while (bShouldRun) { nRetVal = context.WaitOneUpdateAll(depth); //等待数据更新 if (nRetVal != XN_STATUS_OK) //数据更新异常 { printf("Failed updating data: %s\n",xnGetStatusString(nRetVal)); continue; //不间断通信 } //深度数据获取正常 const XnDepthPixel* pDepthMap = depth.GetDepthMap(); //获取数据缓冲区指针 } context.Shutdown(); //关闭上下文对象[/c]

通常配置一个工作链需要实现大量代码,OpenNI提供了工作链查询接口,使得程序员可以通过查询关键字的形式找到系统内已有的相关工作链配置信息,考虑到符合查询条件的结果数量可能不唯一,OpenNI的查询机制结果总是以集合的形式返回。下面给出相关代码:

[c]//创建查询对象 xn::Query query; nRetVal = query.SetVendor("MyVendor");        //设置查询条件,这里设置了有关提供商名称 query.AddSupportedCapability(XN_CAPABILITY_SKELETON);        //这里的查询条件指定数字骨架数据支持 xn::NodeInfoList possibleChains; nRetVal = context.EnumerateProductionTrees(XN_NODE_TYPE_USER, &query, possibleChains, NULL);        //根据查询对象枚举全部的工作链配置 xn::NodeInfo selected = *possibleChains.Begin();        //返回最相似的查询结果,此时仍是但节点信息 nRetVal = context.CreateProductionTree(selected);        //根据节点信息创建相应工作节点 xn::UserGenerator userGen; nRetVal = selected.GetInstance(userGen);        //获取节点实例 ……        //进一步操作[/c]

实际上上述代码在OpenNI编程中非常常见,因为通过编程配置有关工作链实际上是一件很烦琐的事情。但显然对相关节点的查询并不一定能得到理想的结果,有时确实会发生查询结果为零的情况。例如该功能类型的节点尚未配置入系统中,或者节点的许可证序号不一致等异常情况。为方便查看节点测试失败的原因,OpenNI提供了一种针对节点类型的全局测试机制,该机制允许记录节点初始化失败的原因。相关代码如下:

[c]xn::EnumerationErrors errors; //实际上是一个异常结果集合 xn::HandsGenerator handsGen; nRetVal = context.CreateAnyProductionTree(XN_NODE_TYPE_HANDS, NULL, handsGen, &errors); //如果没有一个节点测试成功 if (nRetVal == XN_STATUS_NO_NODE_PRESENT) { //遍历所有的异常信息,并输出 for (xn::EnumerationErrors::Iterator it = errors.Begin(); it != errors.End(); ++it) { XnChar strDesc[512]; xnProductionNodeDescriptionToString(&it.Description(), strDesc, 512); printf("%s failed to enumerate: %s\n", xnGetStatusString(it.Error())); } return (nRetVal); } //如果存在相关节点,但无法验证 else if (nRetVal != XN_STATUS_OK) { printf("Create failed: %s\n", xnGetStatusString(nRetVal)); return (nRetVal); }[/c]

下面这段代码演示了从创建深度生成器,到节点设置配置信息,再从中获取数据并最终显示的全过程。

[c]XnStatus nRetVal = XN_STATUS_OK; Context context; nRetVal = context.Init(); //创建上下文 DepthGenerator depth; nRetVal = depth.Create(context); //创建深度生成器 XnMapOutputMode mapMode; mapMode.nXRes = XN_VGA_X_RES; mapMode.nYRes = XN_VGA_Y_RES; mapMode.nFPS = 30; nRetVal = depth.SetMapOutputMode(mapMode); //设置其输出模式为VGA+30FPS, nRetVal = context.StartGeneratingAll(); //开始获取数据 XnUInt32 nMiddleIndex = XN_VGA_X_RES * XN_VGA_Y_RES/2 + XN_VGA_X_RES/2;  //计算中心像素索引 while (TRUE) { nRetVal = context.WaitOneUpdateAll(depth); //刷新数据缓冲区 const XnDepthPixel* pDepthMap = depth.GetDepthMap(); printf("Middle pixel is %u millimeters away\n", pDepthMap[nMiddleIndex]);  //输出中心像素的深度值 } context.Shutdown();[/c]

音频生成器(AG)的操作与其它节点相比有一定区别,原因在于当程序执行UpdateData()方法后AG才会将之前记录的音频数据存入数据缓冲区中,而且许多情况下音频生成器的数据缓冲区大小是未知的,因此需要经常使用xn::AudioGenerator::GetDataSize()方法获取当前环境下的数据缓冲区大小。下面的例子中首先创建了一个AG,然后对其进行配置,最后读取音频数据。

[c]Context context; nRetVal = context.Init();        //初始化上下文 AudioGenerator audio; nRetVal = audio.Create(context);         //初始化AG XnWaveOutputMode waveMode; waveMode.nSampleRate = 44100; waveMode.nChannels = 2; waveMode.nBitsPerSample = 16; nRetVal = audio.SetWaveOutputMode(waveMode);        //设置AG配置属性 while (TRUE) { nRetVal = context.WaitOneUpdateAll(audio);        //刷新数据缓冲区 const XnUChar* pAudioBuf = audio.GetAudioBuffer(); XnUInt32 nBufSize = audio.GetDataSize();        //获取当前数据大小 } context.Shutdown();[/c]

最后,我们来看看OpenNI提供的录制与缩放功能(NSSDK未提供该功能)。执行录制的前提是首先创建一个Recorder节点对象,并设置好它的待保存文件名。然后应用程序在该节点中添加需要进行录制目标节点,当在Recorder中加入一个节点后,Recorder将首先保存其配置信息,然后对其进行记录。同时Recorder会持续监听目标节点的所有触发事件,以便当节点配置发生改变时能及时获得响应。一旦所有节点添加至Recorder中,应用程序就可以使用Recorder记录节点的数据流了。需要注意的是,针对数据的记录可以使用xn::Recorder::Record()方法,或者通过UpdateAll函数进行。

此外,使用外部XML文件初始化的OpenNI程序可方便地记录其session信息,从而避免修改相应代码。具体只需要在XML文件中为Recorder创建一个新的节点,并将所有节点添加至其中,每当应用程序调用一类UpdataAll函数时均会自动触发记录操作。下面的代码演示了记录一个深度生成器的操作:

[c]DepthGenerator depth; nRetVal = depth.Create(context);          // 创建深度生成器 RetVal = context.StartGeneratingAll(); Recorder recorder; nRetVal = recorder.Create(context);        //创建一个Recorder nRetVal = recorder.SetDestination(XN_RECORD_MEDIUM_FILE, "c:\temp\tempRec.oni");        //配置Recorder属性 nRetVal = recorder.AddNodeToRecording(depth, XN_CODEC_16Z_EMB_TABLES);        //将深度节点加入Recorder while (TRUE) { nRetVal = context.WaitOneUpdateAll(depth);        //注意UpdataAll函数既刷新了数据缓冲区,同时对其中的帧数据进行了记录 }[/c]

录制数据的重放需要首先调用xn::Context::OpenFileRecording()函数,OpenNI读入该文件后先针对文件中的每一个节点创建一个对应的模拟节点,然后使用记录的配置信息进行重配置。应用程序可以随时调用xn::Context::FindExistingNode()获取任何需要的节点,并能像正常节点一样使用。但是应注意,由播放器创建的节点处于锁定状态,且无法被更改,因此其配置信息只能维持记录中保存的状态。同时 ,使用XML文件的OpenNI程序可以轻松替换其载入录制内容,即直接从XML的Recorder节点中读取相应数据。下面的代码就演示了如何载入录制文件,并获取其中保存的深度节点信息。

[c]Context context; nRetVal = context.Init(); //初始化上下文 nRetVal = context.OpenFileRecording("c:\temp\tempRec.oni"); //载入录制文件 DepthGenerator depth; nRetVal = context.FindExistingNode(XN_NODE_TYPE_DEPTH, depth); //获取已录制的深度生成器节点[/c] 在关于OpenNI的最后一篇文章中,我们将重点关注有关节点配置的内容。

Comments