06
May 12

本博方针说明

只发本人原创文章,欢迎转载但请大家注明出处。基本保持至少每周一文的更新频率。内容主要是游戏制作相关的,包括引擎开发,图形学,工具制作,项目管理,读书笔记等。


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字符串,全部从文件读取,避免源代码文件编码问题

参考文献:


14
May 12

Vision and Art ,The Biology of Seeing 读书笔记(2)

继续上周的读书笔记,今天主要讲色彩的感知原理,为什么战地3可以用Chroma SubSampling来提高渲染效率同时又不对效果有很严重的影响?人眼的感光细胞只有10倍左右的动态范围的情况下,为什么我们能同时看清反差如此之大的各种细节?在颜料能够表现的动态范围很有限的情况下,画家们是如何表现高动态场景的?


色彩的感知原理-三原色(Trichromatic) vs 补色(Color Opponent)


就像光具有波粒二相性一样,对颜色是如何构成的,也有两种理论:
  • 三原色,三色中的任何一色,都不能用另外两种原色混合产生,而其他色可由这三色按一定的比例混合出来,这三个独立的色称之为三原色(或三基色)。视网膜上确实有三种感光细胞,分别对三原色敏感。
  • 补色,这种理论认为人对色彩的感知并不是直接编码三种感光细胞的响应,用三原色合成颜色,而是编码某两种原色的差异,所以叫补色原理,在神经生物学的研究中发现,由于三种感光细胞对光线的反应曲线是有重叠的,所以这种编码差异而不是完整的曲线的方式,信息量更小,通过大脑的处理即可还原所有的信息。
一开始这两种理论被认为是不相容的,并且各自都有实验证据,很有点波粒二相性之争的感觉。后来的科学发展证实,这两种理论适用于人脑处理颜色信息的不同阶段,在第一阶段,感光细胞确实是提供了三原色的响应信号。但是在接下来的Bipolar细胞的处理中,就应用了补色的机制。把三原色信号变成了两元补色(two opponent signals)和亮度(luminance)信号。从处理效率和进化的角度来讲,人类还在低等生物的阶段时,只需要亮度信号就可以进行很多有用的信息处理了(Where系统).这样补色信号和亮度信号的分辨率或者说信息量是不同的,并且由于我们先进化出Where系统,后进化处理对颜色信息进行处理的What系统,所以这样划分也很自然,毕竟对进化来说,推翻重构的成本太高,添加功能式演化是比较自然的。有了这样的认识,很多视觉现象就好解释了,后面很多地方会提到。
比如目前应用非常广的图像压缩算法:Chroma SubSampling。从2011年的SIGGRAPH PPT里面我们可以看到战地3也用了这种算法,通过渲染低分辨率的颜色Buffer来提高效率。所以说,Vision and Art就是做渲染的原点,任何严肃的图形程序员都得好好掌握这些知识。


如何在2D的平面上呈现出3D的效果


视网膜需要从两只眼睛获得的2D图像还原出3D的世界,所以需要不少规则去辅助判定
  • 透视(Perspective)
  • Shading(主要是靠物体的亮度差异,而不是颜色)
  • Occlusion(遮挡关系)
  • Haze(远处的物体会由于雾和大气散射而变得模糊)
  • Steropsis(立体视觉)
  • Relative Motion(相对运动)
对画家来说,需要克服已经被大脑处理好的3D图像,而从中去还原出2D图片,然后让其他有着2D视网膜的人觉得这些平面的画很有3D的效果。而克服直觉是需要花很多功夫的,一些画家借助辅助网格,而一些人本身就是由于生理缺陷,缺乏立体视觉。反而成为画家的一大优势。
而为了增加画作的立体感,也需要模拟人眼感知世界的各种规则。下面列举一些我觉得挺有意思的。


透视

人眼因为要从2D图像重建3D的立体世界,所以透视原理也是被硬编码了。就是我们会认为物体必然是近大远小的,比如下面这个著名的Ames’ Window和Ames’ Room。视频演示在这里。如果想自己做一个纸上模型,可以把这个打印出来


Chiaroscuro(明暗法)


这个术语是来自意大利语,直译就是Light-Dark,是绘画的一种技法,用明暗对比来表现物体的体积感。前面提过,人眼的感光细胞对局部的亮度变化很敏感,并且这部分是古老的高分辨率Where系统所掌管的,所以亮度的对比可以给Where系统很强烈的物体轮廓提示,从而让平面的画作获得立体感。而且由于太阳光总从我们头顶射下,所以“光来自上面”这个规则已经被我们的Where系统硬编码了,所以让物体上部亮,下部暗的时候,我们会觉得这个物体很有立体感,反之则是完全不同的深度感觉,比如下面这些球体:
而一旦我们把球体旋转一个角度,上下的明暗对比没有了:
我们就不会有凹凸的感觉了。
再看一个印象派画家的作品,Monet的Rouen Cathedral:The Portal(in sun)。通过刻意的增加局部的明暗对比,教堂的立体感得到非常大的加强。
再来一张安格尔的:Princess Albert de Broglie,对亮度对比的控制已经牛的不行了。


画家是如何表现HDR(高动态范围)的效果


自然界中,不同的物体之间的亮度差异可能会有成百上千倍,但是我们的眼睛可以毫不费力的同时看到这些东西,而实际上,神经生物学家们发现,感光细胞能够编码的亮度差异也就是10倍左右!(这个我以前还真不知道)人眼会通过局部的对比来自动的调整(上一篇文章提到过的Center/Surround原理)。而对画家们来说,使用的颜料动态范围是非常有限的,显然不可能模拟物理真实的世界如此高的反差,于是画家们也逐渐学会了利用局部的对比来匹配人眼的局部性规则。比如伦勃朗的这幅Philosopher in Meditation。通过使用局部的大反差,用低动态范围的颜料模拟了高动态范围的光照效果。实际上,这中技法在实时渲染里面也是后期效果的标配了。

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年开始,他们已经开始探索利用支持多重触摸手势功能的编辑器进行场景制作了。主要有两个项目EdenProton。都是和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,就是鼠标手了…)。感兴趣的同学还可以去论文页面下载演示视频。

参考文献:

  1. Determining the Benefits of Direct-Touch, Bimanual, and Multifinger Input on a Multitouch Workstation

  2. Eden: A Professional Multitouch Tool for Constructing Virtual Organic Environments

  3. Proton: Multitouch Gestures as Regular Expressions


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的值就会出现右边的效果,如果再把这张图加上动画就能有更丰富的效果

 

增强烟雾的体积感的方法

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

最后的效果是这样的:

原始的PPT在这里,和这里


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代码。主要需求是这样的:

  1. 因为材质库是渲染程序和技术美术一起维护的,所以我们不会试图做基于ShadeTree的图形化的材质编辑器,而是材质类型可控的,方便开发的系统。Shader代码还是程序负责实现的,这样效率比较可控,而且比起让美术自己搞Shader,还是建立起完善的材质库比较靠谱。材质是由基础Shader+材质参数+渲染状态构成的,自动生成的是基础Shader代码,材质参数和渲染状态是另外编辑的,构成材质库中的实例。
  2. 不采用动态跳转的UberShader的方式(超长Shader+Constant标记来控制跳转实现不同的算法组合)。而是对每一种组合生成Shader代码。这样的硬件兼容性更好一些。
  3. 能在IDE(比如用VisualStudio)中直接管理Shader代码。通过自定义的Build脚本,生成所有材质的FX文件,这样不同的光照算法,材质LOD,效果分级需要的大量Shader组合的生成就可以自动化了。写代码的效率也会高很多。
  4. 对可调的材质参数,可以定义一些元数据语义,在FX文件中嵌入,然后用数据驱动的方式在我们的模型编辑器中自动生成调节面板,方便美术在导出资源的时候可以快速调节材质参数,并实时预览IN-GAME效果。
  5. 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}
&lt;
#ifdef _SKINNED
    int BonesPerPartition = MAX_BONES;
#endif
&gt;
{
    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项目的开发流程是这样的:

  1. 根据需求,完成一些基础材质的Shader模板
  2. 用VS2010管理Shader代码,可以用一些现成的语法高亮插件,方便代码书写。
  3. 用DX的Effect语义接口(DXSAS)做Shader参数的暴露,方便编辑器整合 。
  4. 在Shader模板中用宏定义一些可以变化的材质标记,比如Glow材质,蒙皮/静态Mesh。但是注意标准材质是需要完整手写的,比如皮肤,金属,布料,头发,这些材质可能就只是就效果级别和LOD自动生成,只是去掉冗余代码提高开发效率的目的。
  5. 用宏定义一个函数实现的各种效果级别。比如高级,中级,低级。这样的好处是都写在一个函数里面,通常这些级别之间的差别就是有没有更复杂的运算和采样,所以能很好的解决冗余代码的问题。由代码生成器去做排列组合。
  6. 用代码生成器生成各种材质组合的各种效果级别的FX文件,代码生成器的配置方式是给出Shader名前缀+需要遍历的材质标记+需要遍历的效果级别 。
  7. 代码生成器可以考虑结合VS自己编译器的Preprocessor来实现,定义成VS的Custome Build Rules。
  8. 生成的Shader原型会加入材质库中,可以由美术用材质编辑器进行实例化,生成不同材质参数的变形,并指定给具体的模型。

这里只是简单的描述了Shader代码的自动生成框架,实际上光有这个还是不够的,还需要和材质系统的整合,比如材质库的建立和管理,所以这个自动生成框架只是一个基础功能,上层还应该有一个材质编辑器来进行具体的材质的指定,而按我们引擎的规划,这个编辑器合并到模型编辑器中比较自然。效果的调试应该放在模型编辑器中进行,而这个编辑器是可以直接由美术的建模工具直接调用的(比如从3DS MAX中调用),让美术可以方便的获得引擎实际效果的反馈。这部分的完整设计以后再写吧。

有价值的参考链接:


22
Mar 12

数据驱动的渲染器设计笔记

最近在进行一些底层模块的重构设计工作,数据驱动是一个大方向,虽然是老生常谈了,但是不得不承认,现在用的引擎架构的设计并不够好,不适合做多核扩展,不适合添加新的API支持,不适合工具的脚本化,并且在热加载方面花的功夫不够,随着功能的增加,资源管线的迭代速度不够快。这些都是需要花大精力解决的问题,现在越发感觉到即时反馈对游戏制作的重要性了。在很多技术决策上,都应该以这个功能为优先。这方面要做的东西很多,今天先写一些渲染器相关的,Shader系统的想法下周再写。因为只是初步的想法,在开始代码实现之后,应该会有很多补充。不管怎么说,记录研发的历程还是很有意思的。

设计目标
1.希望能够用数据驱动的方式配置引擎的渲染流程,兼容不同的渲染框架,比如Forward Lighting , Light Pre-Pass , Deferred Shading , 以及不同的渲染内容,比如场景渲染,反射贴图渲染,角色动态头像,后期处理,特效,UI等。
2.材质系统能够更加灵活,和渲染管线配置绑定,而非直接和可见对象绑定,也就是相同的物件在不同的渲染流程下可以使用同一材质不同的配置。有点类似Visitor模式(Double Dispatch),这样很多逻辑相关的效果切换就变得简单了。并且这些数据最好都保存在物件的元数据集中,方便管理。
3.希望能把平台相关和底层图形API相关的部分做一个很好的封装,以便以后引擎的移植(比如DX9/DX10/DX11)。
4.能够统一各种编辑器和IN-GAME的渲染效果,渲染管线用配置文件生成(比如Lua脚本)并支持热加载。Shader代码也可以进行热加载,方便调试。
5.能够很好的支持多线程行为,比如多线程的CommandList生成。这样可以关闭DX的多线程标记,提高效率。
6.和逻辑系统解耦合,使用数据中间层进行隔离。渲染器的数据和上层逻辑的数据对象相互独立。

渲染器架构
虽然应用程序端实现的渲染器和GPU硬件流水线有很大的差别,但是本质是一样的,都是把算法划分成不同的阶段,数据线性的经过这些阶段的处理,获得最终的结果,类似Pipe and Filters模式。这是一种很常见的设计模式,比如编译器的设计一般也都是这样的,因为都是需要把输入数据,经过若干阶段,变成最终的结果,而对我们上层应用的设计者来说,我们希望这些阶段的配置是灵活的,可以复用的。这样可以满足越来越复杂的渲染算法和更灵活的逻辑处理。

对象关系图
数据对象
ResourceObject:这些对象是用来封装渲染过程中用到的所有资源的,比如模型数据,材质数据,RenderTargets。这些都是属于数据类对象,被其他对象所引用和包含。为了方便处理设备丢失等情况,一些如RenderTaget之类的资源都不直接保存指针,全部用名字做索引。
StateObject:这些对象用来封装渲染状态设置操作,同时也可以提供平台无关抽象
  • 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就可以切换相同模型渲染用的材质,对一些复杂的逻辑驱动的效果就很方便了。
控制对象

Camera:相机对象负责当前场景的Cull操作,获得需要渲染的RenderEntity列表。
ViewPort:管理当前场景的对象,根据游戏逻辑的需求,可以维护多个ViewPort,并进行切换。
RenderStage:RenderStage是根据配置文件生成的,代表渲染管线中的一步,每一个Stage有一个自己的排序规则,输入和输出的RenderTarget和DepthStencilTarget以及对应的材质过滤器。注意RenderStage抽象的是渲染流程,具体渲染哪些东西则是每一个Stage遍历可见对象列表,用Filter根据对象的Effect进行筛选。获得RenderContex对象,然后根据排序方法进行Context的排序。
  • 输出:输出的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来实现会自然一些。
Pipeline:一系列的RenderStage构成了一个渲染管线,每次只能激活一个Pipeline,但是可以根据逻辑的需要随时进行切换,也可以支持热加载操作修改当前激活的Pipeline配置。
RenderDevice:用来封装不同的渲染API的对象,一般来说,分成3大类型的命令,资源管理(创建和删除各种纹理和顶点数据等),GPU状态管理以及管线驱动(比如启动一次绘制操作)。这个对象的实现也是比较模式化的,不用多说什么。
我们可以用这样的Lua脚本配置渲染管线(只是一个初步的设想,我想在实现的过程中,很多细节问题和需求才会冒出来吧)
RenderConfig.lua
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,
	}
}
渲染流程
根据当前相机配置和ViewPort获得可见对象列表,这些对象会被应用到当前的渲染管线中去,获得渲染层的基础数据对象-RenderContex对象,这些对象会进行一次合并排序,并变成API相关的CommandList。在主线程提交这个List就完成了渲染工作。
资源热加载流程
RenderConfig文件可以进行实时修改,比如切换某个Effect的ShaderProgram,或者对管线具体配置进行更改,因为都是采用名字索引,所以只需要重新创建相应的资源和控制对象即可修改渲染效果了。
多线程的支持
填充RenderContext对象可能会比较费时,可以用线程池完成。而CommandList的生成其实也都不用涉及真正的底层设备API的调用,这样这个步骤也可以多线程完成,最后在主线程提交时调用设备API即可,而如果要支持DX11,那么本身就是支持多线程构造DisplayList的。移植起来也是比较方便的。
参考文献