只发本人原创文章,欢迎转载但请大家注明出处。基本保持至少每周一文的更新频率。内容主要是游戏制作相关的,包括引擎开发,图形学,工具制作,项目管理,读书笔记等。
18
May 12
关于Unicode和字符串处理的一些笔记
这篇文章的主要目的是梳理目前的主流Unicode字符串格式相关知识点,包括历史的演化,一些需要注意的事项。
Pre-Unicode
1.ANSI 标准
是最早的单字节编码格式,兼容英语与欧洲语言的显示。0-127是标准字符集,128-255则引入了Code Page的概念。Code Page可以看作一个映射表,不同的国家有不同的Code Page,对相同的128-255的字符有完全不同的映射关系。但是这种编码显然是无法容纳海量的亚洲文字的。
2. DBCS或者MBCS标准
当一个字节已经无法满足字符编码需求的情况下,这种单双字节编码出现了。根据当前系统的Codepage,会约定当遇到某个范围内的字符时,这个字符会被认为是Leading Byte,标记后面的一个字符也是当前表示的一个文字的一部分,需要一起解析,所以使用这种编码,遍历字符串做处理的时候就要小心了,不能直接用s++,s–,否则会出现字符被截断,和后面的字符形成转义字符之类的东西,造成解析错误。由于历史原因,Leading Byte的处理非常繁琐,你不会想自己实现一个的,所以必须使用_mbsinc和_mbsdec等系统API去处理
3.用MBCS支持国际化要考虑的问题
是的,用MBCS也是可以做国际化的,虽然比较麻烦,但是一些遗留的系统和代码如果改成Unicode太痛苦的话,可能用这种方法也是一个Option。主要注意的东西有下面这些:
- 使用_mbsinc和_mbsdec去做字符串指针的迭代操作
- 用_mblen获得字符串长度,比如下面的代码是扫描转义字符的,我们需要对字符串数组进行索引操作,就需要考虑到变长的编码
while ( rgch[ i ] != '\\' ) i += _mbclen ( rgch + i );
- 用_getmbcp 获得当前的CodePage
- 使用_mbscpy拷贝字符串
while( *sz2 ) { //注意如果不是X,那么要使用_mbccmp!! if( *sz2 != 'X' ) { _mbscpy( sz1, sz2 ); sz1 = _mbsinc( sz1 ); sz2 = _mbsinc( sz2 ); } else sz2 = _mbsinc( sz2 ); }
- 正确的字符串比较方式是用系统函数进行,if( !_mbccmp( sz1, sz2) ),如果知道比较的是ANSI字符,则可以直接进行,但是不建议这样做
- 小心Buffer Overflow的陷阱
比如下面这段代码,如果sz是MBCS编码,就有Buffer Overrun的危险
cb = 0; while( cb < sizeof( rgch ) ) rgch[ cb++ ] = *sz++;
正确的做法是这样,注意while里面还需要判断最后一个字符的长度!
cb = 0; while( (cb + _mbclen( sz ))或者直接使用_mbsnbcpy( rgch, sz, sizeof( rgch ) );
参考文献:
Unicode
简要的发展历史
Unicode是一种希望一种编码搞定地球所有语言字符的标准。但是由于种种原因,Unicode也经历了多种版本才总算把有多少字符搞明白。以至于到后来的标准和前面完全就不一致了。1988年Becker发布了第一版Unicode草案,提出用16 bit来表示所有的字符(也就是当时人们认为65535个字符就够地球人用了),91年正式标准发布,也就是UCS-2标准,有很多新的系统和框架使用了这种标准,比如QT,Widnows NT,Java等。但是实际使用后人们才发现,16位根本不够。96年,又出现了UTF-16标准。允许2-4个字节的编码,到目前为止一共有109449个字符,其中CJK占了74500个.
在Unicode标准中所有的字符都用一个叫做CodePoint的唯一数字来代表。但是具体的编码方案可以各异,目前最常用的是三种方式
- UCS-2,把所有的字符用2 Bytes保存,在Windows下wchar_t就是UCS-2
- UTF-16是用2-4 Bytes来保存,为了效率,兼容Big/Little Endian,通过一开头的FE FF Mask位来指定当前文档的字节序。UTF-16是UCS-2的超集。UTF-16的一个字符,有可能在高8位或者低8位上等于0×0,所以不能兼容ANSI标准。这也是Thompson发明UTF-8的主要原因之一
- UTF-8,是Ken Thompson在贝尔实验室参与Plan 9系统开发的时候设计的,<128的CodePoint用一个Byte来编码,然后大于的依次根据其大小用2-6个Byte进行编码。因为兼容ANSI,并且只要做比较少的改动,就能正确的处理多语言支持的问题,所以是Unix/Linux/Web的主流编码格式
Bits Hex Min Hex Max Byte Sequence in Binary 1 7 00000000 0000007f 0vvvvvvv 2 11 00000080 000007FF 110vvvvv 10vvvvvv 3 16 00000800 0000FFFF 1110vvvv 10vvvvvv 10vvvvvv 4 21 00010000 001FFFFF 11110vvv 10vvvvvv 10vvvvvv 10vvvvvv 5 26 00200000 03FFFFFF 111110vv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv 6 31 04000000 7FFFFFFF 1111110v 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv
关于Unicode:
- UTF-8和UTF-16都是变长的编码标准,最大都是4个Bytes(UTF-8定义的时候还有5,6Bytes的情况,不过现在这部分还没有用上,所以可以认为是1-4 Bytes)
- UTF-8是Endianness的,UTF-16分为UTF-16LE和UTF-16BE
- wchar_t在一些平台上是2 Bytes,在一些上是4 Bytes
- UTF-16编码纯中文字符比UTF-8要小一些(2 Bytes,而UTF-8需要3 Bytes),但是一般情况下游戏数据文件还是ANSI字符占大头。所以使用UTF-8编码还是能有效节省带宽和内存的
- 任何byte oriented的字符串搜索算法都可以直接用于UTF-8,编码方式保证了一个字符的字节序是唯一的
- UTF-8不会出现FE FF,也就是不会和UTF-16混淆
- UTF-8可以有BOM用来标志这是一个UTF-8文档,但是现在也有很多文档选择忽略这个标记
- UTF-8可以编码任何Unicode字符,不需要考虑CodePage之类的,能够同时输出到多种语言
- 直接把UTF-8字符串当作unsigned byte进行排序的结果和将其当作Unicode CodePoints进行排序,结果是一致的,这是很方便的设计!
- 没有所谓的Plain Text,必须知道当前的字符串是用什么方式编码的,才能正确的解析。举个例子,对于网页解析来说,首先服务器上由于托管了大量的页面,编码各有不同,所以不能由服务器发编码,那么就交给客户端的浏览器去决定,客户端会先读取页面,去寻找ContentType的字段获得页面的编码,然后再重新Parse整个页面,如果找不到编码方式,不同的浏览器有不同的处理方法,比如IE,就会根据词频之类的特征,猜测当前页面最可能的编码
4.UTF-8实际使用中要考虑的问题
- 工程全部使用UNICODE编译,避免错误的把Narrow String传给Windows API
- 除非特别的指定,所有的std::string和char*都当作UTF-8编码处理
- 可以考虑使用Boost.Locale等高质量的第三方库
- 上层逻辑代码不使用wchar_t,_T(),L”"等,只在底层使用
- 由于MSVC的fstream等文件类,不能支持UTF-8编码的文件名,所以只能使用一个非标准的扩展,就是把UTF-8的文件名转换成UTF-16传给fstream,所以对于文件操作,还是由底层进行封装比较好,比如使用_wfopen_s()。
- 在上层使用的字符串默认都是UTF-8的,底层提供UTF8::Iterator迭代器,供需要进行字符串操作的模块使用。相对MBCS的复杂情况,UTF-8是比较简单的编码,从Leading Byte判定长度是统一的,获得字符的算法也都是字节位操作,还是比较高效的。比如这样:
template <typename octet_iterator> uint32_t next(octet_iterator& it) { uint32_t cp = internal::mask8(*it); typename std::iterator_traits<octet_iterator>::difference_type length = utf8::internal::sequence_length(it); switch (length) { case 1: break; case 2: it++; cp = ((cp << 6) & 0x7ff) + ((*it) & 0x3f); break; case 3: ++it; cp = ((cp << 12) & 0xffff) + ((internal::mask8(*it) << 6) & 0xfff); ++it; cp += (*it) & 0x3f; break; case 4: ++it; cp = ((cp << 18) & 0x1fffff) + ((internal::mask8(*it) << 12) & 0x3ffff); ++it; cp += (internal::mask8(*it) << 6) & 0xfff; ++it; cp += (*it) & 0x3f; break; } ++it; return cp; }
- 所有的数据文件也都使用UTF-8编码
- 代码中不直接出现UTF-8字符串,全部从文件读取,避免源代码文件编码问题
参考文献:
- http://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt
- http://programmers.stackexchange.com/questions/102205/should-utf-16-be-considered-harmful
- http://utf8everywhere.org/
- http://www.codeproject.com/Articles/14637/UTF-8-With-C-in-a-Portable-Way
- http://blogs.msdn.com/b/michkap/archive/2009/06/29/9800913.aspx
- http://en.wikipedia.org/wiki/Utf-8
14
May 12
Vision and Art ,The Biology of Seeing 读书笔记(2)
- 三原色,三色中的任何一色,都不能用另外两种原色混合产生,而其他色可由这三色按一定的比例混合出来,这三个独立的色称之为三原色(或三基色)。视网膜上确实有三种感光细胞,分别对三原色敏感。
- 补色,这种理论认为人对色彩的感知并不是直接编码三种感光细胞的响应,用三原色合成颜色,而是编码某两种原色的差异,所以叫补色原理,在神经生物学的研究中发现,由于三种感光细胞对光线的反应曲线是有重叠的,所以这种编码差异而不是完整的曲线的方式,信息量更小,通过大脑的处理即可还原所有的信息。
.png)

- 透视(Perspective)
- Shading(主要是靠物体的亮度差异,而不是颜色)
- Occlusion(遮挡关系)
- Haze(远处的物体会由于雾和大气散射而变得模糊)
- Steropsis(立体视觉)
- Relative Motion(相对运动)
.png)

.png)




06
May 12
Vision and Art ,The Biology of Seeing 读书笔记(1)
这本书入手也有一段时间了,术语比较多,读起来比较头疼,不过习惯了到后面就好了。印刷十分精美,各种插图设计也是一流的,看起来还是很享受的。全书的中心思想就是:Vision is information processing, and visual computations are SIMPLE, LOCAL, and OPPONENT。视觉并不是对外部世界的真实还原,在进化的过程中,人脑还形成了很多信息加工的功能,方便快速的获得某种重要信息,比如人脸的识别,深度的感知,颜色从2元到3元以便能识别出有毒的果实之类的。而很多画家其实就是利用了人脑的这些处理信息模式,创造出了经典作品。下面记录一些有意思的地方。
视网膜的构成

视网膜(Retina)的构成如上图所示,列一下从维基百科上拷贝过来的中英文的术语对照,这样后面好理解一些:
- 视网膜色素上皮 (retinal pigment epithelium)
- 感光层 (photoreceptor layer,rod,cone) ,包括视杆和视锥细胞
- 神经节细胞层 (ganglion cell) ,这个层含有神经节细胞的细胞核,视神经从这里开始
- 内核层 (inner nuclear layer),又称内颗粒层,由双极细胞(Bipolar Cell)、水平细胞(Horizontal Cell)、无长突细胞、Muller细胞的胞核组成
- 神经纤维层 (nerve fiber layer),主要为神经节细胞的轴突
比较有意思的地方:
- 感光层居然在Retina的最外层,也就是最后才接收到光线的,也就是这个原因,在中心的Fovea区域,为了最大化分辨率,这里的三层神经节细胞都被移开了,露出了一小块区域,让光线经历的中间细胞少一些
- 中间的几层神经元细胞会在感光层接受光线刺激产生信号后,对信号做整合预处理
- 光感受体传递神经信号的方式和普通的神经并不相同,光感受体平时一直处于Potentiate的高电位状态,光线刺激出现的时候才降低电位
- 视锥细胞工作在比较亮的环境下,而且可以分辨颜色。视杆细胞工作在比较暗的环境下,其分辨率比较低,而且不能分辨颜色
Center/Surround原理

在感光细胞送出神经元信号后,神经节细胞层会对信号进行横向整合,遵循的是著名的Center/Surround原理,也就是在中央区域的光线刺激是+信号,而在边缘区域的光线刺激是-信号。这样的组织下,人眼的中心视觉灵敏度很高,并且对运动和变化的物体反映十分灵敏,也就是对光的变化敏感,而不是总体的亮度。这恐怕也是进化过程中必需的。而且重点编码运动和变化的信息,而不是整体的信息,也是一种信息压缩的手段吧。下面这张图可以让我们直观的感受到Center/Surround预处理的效果,当把视觉焦点放在白线交汇处时,会发现周围的交汇处有黑点闪烁,并且离中心视觉焦点越远越黑,当视线移动时,还能明显感觉到闪烁,这是因为神经信号传递的时间延迟。

中心视觉和边缘视觉
人眼的中心视觉分辨率很高,相对的,边缘视觉则要粗很多,但是比较擅长分辨大的轮廓和重要的元素,比如人脸。下面这张对蒙娜丽莎的脸部分析图可以很直观的解释为什么她的微笑那么神秘。


当你聚焦到她的脸部之后,视觉分辨率很高,看到的都是细节,也就是最右边的效果,所以微笑并不明显,但稍微移动视线,由于边缘视觉是看轮廓的,看到的就是左边的两种效果,所以就会突然觉得笑容更明显了。很多画家都会运用人眼的这种视觉原理去创造出一种变化感和运动感。特别是印象派的。

莫奈的这张Rue Motorgueil in Paris,Festival of June 30 , 1878.也是运用这种原理制作的,创造动感并不是单单的Blur掉。而是精确的模拟人眼的Peripheral Vision的Spatially imprecise的特点。
What and Where System

如上图所示,大脑中有两套处理视觉信息的系统,其中where系统是比较古老的构造,特点是分辨率不高,对反差很敏感,色盲,主要依靠亮度信息来判定深度和物体的运动,人物和地面的分离。而what系统是后期进化出来的,分辨率高,对颜色敏感,低反差。用来进行对象识别,人脸识别和颜色感知。
利用这个特点,我们就有了Equiluminant Color,指色调(Hue)和饱和度(Satureate)不同,但是亮度(Luminance)相同的色彩,用这种色彩组成的画面,会导致人的Where系统无法对其中的形状进行定位,所以会有闪烁,漂浮不定的感觉,也是很多画家利用的一个特点,比如印象派的很多画家

莫奈的这幅日出印象就是典型,也包括梵高的很多作品。从图上可以看到莫奈的太阳有比较特别的动感,但是把颜色去掉之后,我们就发现太阳的Luminance和背景基本是一致的。不过我觉得网上找的图效果都不明显,因为Where系统灵敏度很高,只要有亮度差异,基本就识别出来了。而且和屏幕的校准也有关系,这里有个网站可以实时的调整看效果。


今天先写到这里,剩下的下周继续。
Update 2012.5.16,第二部分在这里
29
Apr 12
支持多重触摸手势的场景制作工具

Pixar一直是我很喜欢的工作室,技术和艺术的结合堪称完美,他们的研究院主页上有很多有意思的Paper,最近有几篇和利用新一代人机交互技术来提高制作效率的,还比较有趣,随着支持多重触控的平板设备普及,也许在不久的将来,这些技术也能运用到游戏制作的编辑器里?虽然没有Pixar内部实际如何运用的信息,但是从09年开始,他们已经开始探索利用支持多重触摸手势功能的编辑器进行场景制作了。主要有两个项目Eden和Proton。都是和Berkeley合作的项目,主要解决的问题是电影中有大量物件的3D场景制作效率( set-dress 3d scenes)。Eden是多重触摸界面的编辑器,而Proton是为了支持大量复杂手势的定制和自动匹配的工具,可以说技术上已经比较成熟了。我们都知道目前不管是电影还是游戏的主流的场景制作工具还是类似Maya这样的,完全基于鼠标和键盘的传统UI交互。Eden就是想颠覆这个传统,Berkeley的研究人员+10年经验的Pixar场景美术的团队。09年首先是进行了科学的测试,证明多重触控的操作确实能够有效的提高效率,论文在这里。于是首先开发了Eden,论文里面列出的多重触控手势设计原则还是比较有意思的,以后做平板的软件说不定能用得上,记录一下:
- Use simple gestures for frequently used operations
- Conjoined touch as a modifier,引入Conjoiner Touch的概念,使得两个手指的触摸操作可以表示三种语义,方便操作
- One operation at a time,两只手分别操作多个物体的需求很少,而且不直观
- Split touches across both hands
- Use at most two fingers from each hand,不是每个人都是练钢琴的,所以每只手最多用两个手指
- Interchangeability of hands,手势是可以互换完成的
- Motion of gesture reflects the operation,手势反映出操作的特点,方便记忆与实施
- Combine direct and indirect manipulation,直接和间接操作需要结合使用
- Control at most two spatial parameters at a time,每次最多控制两个空间参数,这也是和美术实际测试下来最方便的操作
最后Eden测试的结果是,制作效率提升20%,由于工作分摊到两只手上,可能可以减轻老美术的RSI(repetitive stress injury,就是鼠标手了…)。感兴趣的同学还可以去论文页面下载演示视频。
参考文献:
22
Apr 12
对游戏制作层面问题的一点思考
制作层面的问题非常多,今天写一个小点,就是工具化的问题。做过大规模项目的人都知道,总是有一些职位,在整个项目中特别重要,但是由于各种原因,比如事情特别琐碎,重复性劳动比较多,未来也没有明确的发展方向,所以大家都不愿意做。这就是典型的“垃圾职位”。举个游戏制作的例子,比如版本发布。往往涉及非常多的组,资源也比较复杂,而且往往是加班最多的,但是这个工作本身又没有什么技术含量,之前经历的一个项目,做这个工作的是QA组的毕业生小伙。由于他干的不错,虽然也出过事故,但是由于没有人愿意接手,也就这么干下来了,后来虽然做了一整套工具去把大部分工作都自动化了,但是这个职位似乎是理所当然需要继续存在的。现在回头想想,什么样定位的人愿意做这样的工作?在早期没有工具化的情况下,人肉干掉这些Dirty Work是必须的,但是我们不能把这样的工作固化,而是应该在开发有余力的情况下,把这样的工作工具化,减少上手度,从而能够把这种工作分摊到任何人手上。
对流程的优化应该是一个动态过程,不应该墨守成规。看一个团队的研发实力,其实就是看这个团队的工具做的好不好。而“工具化”其实是一个很重要团队文化。拿我们引擎组来说,每个新人必须掌握至少一门动态脚本语言,比如Python,并且鼓励遇到问题宁愿多花一点时间写脚本解决,这样以后遇到类似的问题就不用再手动实现了。这样慢慢的也积累了很多小工具,而且由于就是解决自己日常工作中繁琐事务的工具,大家也愿意去不断修改和完善。
再举一个反面例子,之前一个项目的调试控制台的开发,控制台开始是专门的工具组制作的,通过脚本扩展各种调试命令,交付给策划和系统逻辑程序使用。后面效果并不好,现在看来主要几个核心原因
- 控制台的后续开发并没有列在项目计划中
- 工具组的人不参与逻辑系统开发,并没有大量调试需求,从而不会主动去改进控制台
- 系统逻辑组的人认为控制台应该由工具组完善和发布,两边沟通的障碍导致这个工具没有专门的发布和维护人员,造成后期一些调试命令失效后,只是由需要用到这个功能的策划找自己熟悉的程序去修复,而这个修复的版本也没有统一发布,结果其他策划并不知道功能修复了
- 系统设计没有贯彻方便调试的原则
怎么解决这种问题,也有几种思路:
- 工具的开发应该列入项目计划,工具是产品的一部分,并且需求是自上而下传达,也就是设计某个模块的时候,Leader就应该把相关工具的需求也一并提出,交给下面负责实现的人员完成
- 上面这种方法相当于把工具需求放到Leader级别去控制,虽然对大工具是不错的解决方法,但是对于一些小工具,可能只有具体实现的程序才会有好的工具想法。所以如果能把工具化当作一种团队文化去贯彻,结合第一种方法,应该是最理想的了
暂时写这么些,工具化的思想和实时反馈方便调试的思想是一致,一般来说,做不好工具化,实时反馈也做的不好。而对游戏制作来说,迭代速度越快,让修改内容的人越快获得反馈,意味着更高的质量,更好的游戏。顺便提一下,神级程序出身的前苹果UI交互设计师Bret Victor的Inventing on Principle非常值得一看。
15
Apr 12
GDC2012 神秘海域系列的特效技术讲座笔记
UC1的特效系统是基于宏语言描述的数据脚本,采用了6000+行的UberShader,利用PPU和SPU进行计算。实际用下来感觉UberShader太难维护了,而且要美术去写特效脚本,不是非常直观。美术对这个系统的感觉是suck the soul out of me…所以说嘛,技术都是积累出来的。

所以新的特效系统做了完全的重写,以大幅度提高迭代效率为设计目标,更加的数据驱动,采用基于节点的Shader编辑器方便美术自己调效果

特效编辑器是嵌入在MAYA中的,这方面顽皮狗也是有很多积累的,感觉也是一个不错的工具思路。

美术在MAYA中直接编辑粒子的发射器,Expression脚本和曲线等属性,然后输出给用Scheme写的DataCompiler变成VM Byte,再由网络层传输给引擎进行可视化,用网络层隔离引擎和工具也是数据驱动的一个常用手段,这样工具选择什么语言跑在什么系统下都没什么关系了。

Shader编辑器是SONY ATG完成的,然后顽皮狗拿过来做了定制,基于节点的编辑方式,每一个节点是一个CG函数,也可以插入自定义的函数,美术自己管理Cycle预算,也就是美术会控制Shader生成的长度,严格的按照性能指标来进行

特效在游戏中发射方式
5种方式可以发射粒子:场景编辑器中摆放,脚本,动画中的关键帧和挂点,水面,以及代码中直接发射

在场景中摆放的发射器:

在动画序列中绑定的发射器:

在水面上绑定发射器:


特效的更新步骤:

在preupdate中进行碰撞检查

为了提高粒子碰撞效率,做了很多优化,每一个粒子保存一个碰撞平面,在每一帧选部分粒子进行碰撞检查,结果在下一帧返回并存储在粒子数据结构中,粒子的碰撞信息是Round Robin式更新的,保证每一帧的开销比较小,结果不太精确但实际上很难注意到

Update阶段执行DC从Expression脚本编译成的VM代码,改变粒子的颜色,位置,自定义属性

UC3的Decal的做法
简单的说就是用一个Bounding Box获得屏幕上需要绘制的像素范围,然后在PS中采样Depth Buffer获得世界坐标的位置,并变换到particle space(projection space of particle),同时在Light PrePass的时候把Decal的法线也渲染到Normal Buffer中去。感觉和Humus的这个Demo做法差不多


用Stencil Test去避免把粒子贴地投射到前景物体上去


UC3的大火的Trick
相信玩过神海3的人对大火的场景印象还是比较深的,他们一开始也是用常规的粒子来实现的,结果FPS只有12,实在跑不动,于是发展了一个比较有意思的技术,就是用很少的面片,但是用一张纹理来扰动Z深度,造成有前有后的效果,比如下面这张图片,其实就两个关闭Z Test的面片,但是感觉还不错:

这个技术要求所有的动态效果都通过纹理来控制,并且对纹理精度要求比较高,否则就糊了。灵感是从大家都很熟悉的软粒子实现机制来的,下面左边的就是Z-bias和Z-blend的效果,如果用一张纹理来给出Z-bias的值就会出现右边的效果,如果再把这张图加上动画就能有更丰富的效果


增强烟雾的体积感的方法
其实很简单,就是对法线图进行点乘,因为对法线的方向进行了修改,所以实际上就是控制高光和阴影


最后的效果是这样的:

01
Apr 12
GPU Pro 3 CryEngine3 Three Years of Work in Review读书笔记
GPU Pro系列感觉越来越水了,比如这次的Engine Design部分,Z3 Cull和Quaternion Pipeline以及Data Driven Renderer都比较水,这种文章按GPU Pro 1的标准肯定是进不来的吧。不过下面要写的这篇文章感觉还是很有料的,所以特地做了笔记,其实里面的内容以前都多多少少看过,不过这次是比较系统的总结,还是值得重读的。
CryEngine3 Three Years of Work in Review Notes
多平台的挑战
1.在刚开始进入的时候,大部分人都没有单机游戏开发经验,最开始的PS3版本,后期就要花30ms,粒子50ms,只能跑10帧!
2.低配的重新设计,在Crysis1中,低配关闭了大部分特性,而在Crysis2中,一开始就考虑到低配的效果,控制了和高配的差距
3.高层优化心得,虽然看起来都很简单了,不过一开始做的时候还是很难全部考虑
- 寻找最大的瓶颈,避免部分优化,比如Motion blur很费,就在静止的时候禁用,但是一旦移动,就会造成严重的掉帧
- 尽可能共享计算结果,不要重复计算,Xbox360上一次720p的全屏后期至少就是0.25ms,PS3则是0.4ms。尽可能降低内存传输量,RenderTarget的Clear操作
- 一个Pass尽可能多的Batch
- 利用帧间的连续性,把开销平摊到多帧
- 把优化工作流程化,不要专人去负责,而是每个工程师都有义务优化性能和内存。包括美术和策划也要参与优化。为了方便这个工作,开发了大量的可视化工具,让大家可以直观的看到各个部分的开销。在资源铺量制作开始之前,就把性能的Budget定义好,并且保证不光是程序,美术和策划也都理解这些性能指标
- 定期(非常高的频率)做详细的性能分析工作,用PIX/GPad对所有平台和所有配置的性能数据进行分析,保证性能不会失控。引入了一个Visual Regression Tests。每次程序提交代码,就会触发编译机自动编版本,然后在固定的相机视角和固定的场景截图,抓取性能数据,自动上传到Web平台中,方便监控,当然还不是完全自动化的,比如不能自动比较截图分析是否有渲染错误出现
基于物理的渲染(Physically Based Rendering)
- CE3还是想保持尽可能少的预计算的光照算法,并能支持大量的动态光源,Deferred Shading对于单机来说,还是太费了,并且不能支持很丰富的材质也是一个大缺点,所以采用了Deferred Lighting(Light Pre-Pass)的算法。
- G-Buffer的构成
-
- RT0:Normal+Glossiness
- RT0:Diffuse Accumulation
- RT1:Specular Accumulation
- RT0:Scene Target
- HDR的重要性:光照从本质上说是高动态范围的,人眼的宽容度也是很高的,所以光照管线走HDR是非常有意义的,可以有效的避免Color Clipping。也可以简化美术的工作流程,避免很多LDR的Workaround。
- 线性渲染管线的重要性,这个之前的一篇文章总结过。
- Deferred Lighting
-
- 从深度信息还原世界空间/屏幕空间的位置:这个算法总结的挺简洁的,之前也看过,主要就是三个basis vector,先从相机矩阵获得vXBasis,vYBasis,vZBasis,然后变换到想要进行计算的local space,比如阴影,最后用VPos进行插值即可得到齐次空间的位置
- AmbientPass:Lighting_Accumulation的第一个Pass,叫Hemisphere Lighting,其实就是ambientColor * ( N.z * 0.7 + 0.3)。看到这个我想起我们之前项目的光照模型了,当时我们加了一盏顶光,来表现明暗对比,其实想起来,不就是这种东西么。实际效果出奇的好的,还简单。这就是trick啊。
- Environment Lighting Probes:这个主要就是基于图像的光照了,也是一个创新点,通过手动的在场景中摆放一些采样球,在这些地方渲染一张低分辨率的CubeMap来捕捉场景中的Diffuse和Specular光照信息,并用RGBK(RGBM)编码。这个图是用ATI的CubmapGen工具生成的(前段时间开源了,并且解决了接缝问题)。这个方法非常值得尝试啊。目前引擎也用了球谐来处理这个东西,但是主要是给地宫的静态面光源照亮动态物体用的,其实把这个流程扩展扩展,探索一下这种方法也不错。
- Diffuse Global Illumination:这个就是Light_Propagation Volume了,之前的帖子分析过了。
- Contact Shadows:就是解决SSAO没有带方向信息,只是减弱环境光的问题,引入了Screen Space Directional Occlusion。在计算AO的Pass中,多保存一个bent_normal。在随后的光照计算中,用这个来影响光源的强度。
- Real Time Local Reflections:在DX11的升级中加入的这个特性,在屏幕空间对每个像素通过其世界坐标法线计算出一个反射方向,然后Ray Marching多个采样点,如果发现有Hit,那么就对上一帧的back buffer进行采样获得反射点的颜色。实际情况还是比较复杂的,比如视点突变造成的Popping,采样点的jitter处理减少走样之类的。不过效果确实是很棒的。
- Light Pass:光照模型就是传统的Blinn-Phong,支持点光与Projector光源,同时投影的光源最多有4个。通过美术手动的给某些光源添加Clip Volume来做Stencil Culling,避免Bleeding。
- Shadow:阴影渲染是用的Cascaded Shadow Map,对于点光源,会生成6个Frustum,每一个对应一张大小不同的阴影图,合并成一张大的atlas
- Deferred Shadow Pass:用Stenciling来标记出可能接受投影的屏幕区域,避免在单机上非常费的动态跳转。对于低配的PC和单机,Cascade Shadow Map是分帧更新的,远处的阴影图更新和近处并不同步。
- Transparent_Shadow:为了支持半透明的投影,比如粒子特效的投影,另外用了一个RenderTarget来累积半透明物体的的Alpha值。Depth_Testing还是需要采样不透明的ShadowMap,避免back_projection。最后阴影的强度是取Max(translShadow,opaqueShadow)获得的
- Deferred Decals:传统的Forward贴地需要动态生成几何体,额外的内存和CPU计算开销,和DX11的Tesselation也不匹配,所以走的是延迟管线渲染,并且只能投射到静态物体上,这样避免了任何动态几何体生成。
- Forward Shading
-
- Light Buffer格式:PS3上由于浮点性能问题,用的是RGBM格式,PC上直接上16位浮点了。这样效果当然好了。
- 这个pass也叫geometry pass,就是利用之前的光照GBuffer来进行实际的材质方程求解获得最终的颜色了。
- Deferred Skin Shading:也就是在屏幕空间做Subsurface Scattering。思想很简单,就是在Geometry pass的时候,做一个Kernel大小和深度相关的Poisson分布的Bilateral Blur。因为角色占的像素相对较少,所以效率还可以接受
- Screen-Space Self Shadowing:在屏幕空间做角色自阴影,就是从深度Buffer还原坐标,沿光照方向进行Ray Marching,也是比较费的。但是效果很不错,最后皮肤,头发和眼睛的自阴影都是用这个方法算的
- Soft Alpha Test:大多数游戏都会在头发上避免使用Alpha混合。CE3很巧妙的解决了这个问题,就是在屏幕空间的Tangent方向进行Per-Pixel Blur,同样的算法也能用来处理Fur,不过要沿着屏幕空间的Surface Normal做Blur。
30
Mar 12
Shader自动生成框架
之前项目的Shader是纯记事本手工打造的,随着材质类型增加,包含了大量的冗余代码。举个简单的例子,比如下面这些Shader:
- SceneBlinnSkinnedHigh/Med/Low
- SceneBlinnStaticHigh/Med/Low
- SceneBlinnSkinnedGlowHigh/Med/Low
- SceneBlinnStaticGlowHigh/Med/Low
如果还想再加入一种新的光照算法,那么对静态/蒙皮,3种效果级别,都需要排列组合一遍。后期维护成本非常高。所以希望能够把这部分机制做一次重构,能够把冗余代码消除,方便的增加新的算法,自动生成不同组合的Shader代码。主要需求是这样的:
- 因为材质库是渲染程序和技术美术一起维护的,所以我们不会试图做基于ShadeTree的图形化的材质编辑器,而是材质类型可控的,方便开发的系统。Shader代码还是程序负责实现的,这样效率比较可控,而且比起让美术自己搞Shader,还是建立起完善的材质库比较靠谱。材质是由基础Shader+材质参数+渲染状态构成的,自动生成的是基础Shader代码,材质参数和渲染状态是另外编辑的,构成材质库中的实例。
- 不采用动态跳转的UberShader的方式(超长Shader+Constant标记来控制跳转实现不同的算法组合)。而是对每一种组合生成Shader代码。这样的硬件兼容性更好一些。
- 能在IDE(比如用VisualStudio)中直接管理Shader代码。通过自定义的Build脚本,生成所有材质的FX文件,这样不同的光照算法,材质LOD,效果分级需要的大量Shader组合的生成就可以自动化了。写代码的效率也会高很多。
- 对可调的材质参数,可以定义一些元数据语义,在FX文件中嵌入,然后用数据驱动的方式在我们的模型编辑器中自动生成调节面板,方便美术在导出资源的时候可以快速调节材质参数,并实时预览IN-GAME效果。
- Shader的排列组合是可控的,而不是完全自由组合,遍历所有的可能性,因为这样有可能会造成自动生成大量无用的Shader。所以Shader的生成是用配置文件控制的,和材质库规划相关的。
实现思路
对一类可以进行明确分类的材质,写一个Template.fx文件,在这里面定义可以进行变化的预处理宏,以及相应的代码,比如下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | ---SceneObjectTemplate.fx--- VS_OUTPUT VSSceneModel( #ifdef _SKINNED float3 inBlendWeights : BLENDWEIGHT0, int4 inBlendIndices : BLENDINDICES0, #endif float3 iNormal : NORMAL0, float2 iTex : TEXCOORD0, float3 iPos : POSITION0 float3 inColor : COLOR) { #ifdef _SKINNED ....Skinned Codes #else ....Static Codes #endif Common Codes } .................. .................. .................. technique {FileName} < #ifdef _SKINNED int BonesPerPartition = MAX_BONES; #endif > { pass p0 { #ifdef _HIGH VertexShader = compile vs_3_0 VSSceneModel(); PixelShader = compile ps_3_0 PSSceneModel(); #else VertexShader = compile vs_2_0 VSSceneModel(); PixelShader = compile ps_2_0 PSSceneModel(); #endif } } |
每一个Template文件实际上定义了一组technique的生成规则。名字需要通过外部的预处理配置文件决定,这里只是一个占位符。比如要生成场景物件蒙皮的带Glow不带光照图的高级效果Shader。那么预处理器就需要定义 _SKINNED _GLOW _HIGH。并且需要定义名字的字符串组合,用来生成最终的Shader Technique的名字,这可以定义在一个同名的元数据文件中:
1 2 3 4 5 6 | ----SceneObjectTemplate.cfg---- Prefix=”SceneModel“; Switch1=_SKINNED;_GLOW; Switch2=_RGBM_LIGHTMAP; Switch3=_SH_LIGHT; Swtitch4=_HIGH;_MED;_LOW; |
在shader_gen.py中,对cfg中定义每一组Switch,进行排列组合。动态生成预处理器bat文件,生成对应的FX文件,FX文件名也就是Shader Technique名字。需要完成预处理后进行替换。预处理脚本是这样的,我们可以直接用C编译器的预处理功能来实现,无须自己重新写脚本了。
1 2 3 4 5 6 7 8 9 10 11 | ----Preprocessor.bat---- @echo off echo 删除旧文件 SceneModelSkinnedGlowHigh.fx REM del /F SceneModelSkinnedGlowHigh.fx echo 根据宏从模板%1进行FX代码生成,目标文件: SceneModelSkinnedGlowHigh.fx cl -EP -D_SKINNED -D_GLOW -D_HIGH %1 > SceneModelSkinnedGlowHigh.fx echo 对自动生成的FX文件进行美化排版 SceneModelSkinnedGlowHigh.fx REM configuration of GC is found in GC.cfg GC -file-SceneModelSkinnedGlowHigh.fx |
Shader项目的开发流程是这样的:
- 根据需求,完成一些基础材质的Shader模板
- 用VS2010管理Shader代码,可以用一些现成的语法高亮插件,方便代码书写。
- 用DX的Effect语义接口(DXSAS)做Shader参数的暴露,方便编辑器整合 。
- 在Shader模板中用宏定义一些可以变化的材质标记,比如Glow材质,蒙皮/静态Mesh。但是注意标准材质是需要完整手写的,比如皮肤,金属,布料,头发,这些材质可能就只是就效果级别和LOD自动生成,只是去掉冗余代码提高开发效率的目的。
- 用宏定义一个函数实现的各种效果级别。比如高级,中级,低级。这样的好处是都写在一个函数里面,通常这些级别之间的差别就是有没有更复杂的运算和采样,所以能很好的解决冗余代码的问题。由代码生成器去做排列组合。
- 用代码生成器生成各种材质组合的各种效果级别的FX文件,代码生成器的配置方式是给出Shader名前缀+需要遍历的材质标记+需要遍历的效果级别 。
- 代码生成器可以考虑结合VS自己编译器的Preprocessor来实现,定义成VS的Custome Build Rules。
- 生成的Shader原型会加入材质库中,可以由美术用材质编辑器进行实例化,生成不同材质参数的变形,并指定给具体的模型。
这里只是简单的描述了Shader代码的自动生成框架,实际上光有这个还是不够的,还需要和材质系统的整合,比如材质库的建立和管理,所以这个自动生成框架只是一个基础功能,上层还应该有一个材质编辑器来进行具体的材质的指定,而按我们引擎的规划,这个编辑器合并到模型编辑器中比较自然。效果的调试应该放在模型编辑器中进行,而这个编辑器是可以直接由美术的建模工具直接调用的(比如从3DS MAX中调用),让美术可以方便的获得引擎实际效果的反馈。这部分的完整设计以后再写吧。
有价值的参考链接:
22
Mar 12
数据驱动的渲染器设计笔记
- RenderState:各种渲染管线状态Get/Set操作的数据对象。
- RenderEntity:这种类型的对象是负责组装底层的资源型对象获得一个特定的渲染实体对象,这些资源对象是可以被共享的,比如我们会把一个场景块中所有静态模型的顶点数据全部合并到一个Vertex Buffer中。根据包含的资源的不同,分为不同的类型,比如场景模型,特效,UI等。
- RenderContext:渲染状态相关辅助对象,包含一次绘制操作所需要的RenderState集合。从逻辑层获得的渲染对象最终变成一组Context对象。这些Context对象进行排序合并之后变成特定的API的Command List,最后在主线程执行这个List就完成了绘制操作。
- CommandList:和Context不同,CommandList就是具体的API相关的渲染命令列表了。这一层就是API相关的了。把Context和Command分离开,可以给我们多一层的间接性。
- Effect:封装可见对象的材质表现,一个Effect实例包含了某类可见对象的全部渲染材质,比如场景模型的阴影渲染材质,半透明状态的渲染材质,RenderStage会根据材质名过滤得到RenderContext的列表,这样切换RenderStage就可以切换相同模型渲染用的材质,对一些复杂的逻辑驱动的效果就很方便了。
-
输出:输出的RenderTarget和DepthStencilTarget名字,索引全局资源池中的对象
- 排序方法:当前Stage中包含的RenderEntity的排序方法
- Filter:表示当前的Stage的材质属性,比如Shadow,Decal,DebugRender,GBuffer等,材质系统的Effect对象会根据这个Filter选择合适的Shader填充到RenderContext中去。
- 算法回调对象:在Stage的单一Pass的渲染之外,还可以关联一个算法回调对象Compositor,可能是组合了多次Pass的渲染流程,也有可能是比较特殊的算法,比如后期处理需要多次绘制全屏的Quad,每次做不同的处理。每一个Pass的输出会成为下一个Pass的输入。不太适合用Entity/Effect/Filter的框架处理,所以变成Compositor来实现会自然一些。
Effects =
{
SceneModel=
{
Shadow =
{
Filter = "DepthShadow",
Program = SceneOpaqueShadow,
}
OpaqueNormal =
{
Filter = "Opaque",
Program = SceneOpaque,
}
OpaqueHighLight =
{
Filter = "HitHighlight",
Program = SceneOpaqueHighlight,
}
SemiTransparent =
{
Filter = "SemiTransparent",
Program = SceneSemiTransparent,
}
}
}
Pipeline=
{
Shadow=
{
Type = Stage ,
Filter = "DepthShadow" ,
RenderTargets = "depth_f32_buffer" ,
DepthStencilTarget = "depth_stencil_buffer",
Sort = "front_back",
}
Ground=
{
Type = Stage ,
Filter = "OpaqueGround" ,
RenderTargets = "scene_buffer" ,
DepthStencilTarget = "ds_buffer",
Sort = "front_back",
}
Bloom=
{
Type = Compositor ,
Config = "BloomCompositor",
}
}
BloomCompositor=
{
DownScale=
{
Type = "FSQuad" ,
Shader = "Bloom_DownScale" ,
Inputs = "scene_buffer" ,
Output = "scene_scaled_buffer" ,
}
GaussianVertical=
{
Type = "FSQuad" ,
Shader = "Bloom_VerticalBlur" ,
Inputs = "scene_scaled_buffer" ,
Output = "scene_bloom_vertical" ,
}
GaussianHorizontal=
{
Type = "FSQuad" ,
Shader = "Bloom_HorizontalBlur" ,
Inputs = "scene_bloom_vertical" ,
Output = "scene_bloom_horizontal" ,
}
Gamma=
{
Type = "FSQuad" ,
Shader = "Bloom_Gamma" ,
Inputs = "scene_bloom_horizontal" ,
Output = "scene_gamma" ,
}
Combine=
{
Type = "FSQuad" ,
Shader = "Bloom_Combine" ,
Inputs = {"scene_gamma","scene_buffer"} ,
Output = "back_buffer" ,
}
}
ResourcePool=
{
depth_stencil_buffer =
{
type = "render_target",
format = "D24S8",
size_ref = "back_buffer",
w_scale = 1,
h_scale = 1,
clear = true,
}
}
- GPU Pro 3 Designing a Data-Driven Renderer
- Bitsquid GDC 2012
- DX9多线程标记
