SHADER的动态组合

 Shader是很奇怪的代码,它的长度受到限制,它的动态分支能力很弱,它的指令很昂贵,这些都使得你很难使用一个单一的Shader来处理所有的渲染要求.而各种渲染要求的种类如此之多,如果要为每一种渲染类型都写一段专一的代码的话,那会是一件非常吃力的活,假设我们现在要写一个材质系统,我们希望它能够支持各种效果.你会发现随着支持的效果越来越多,需要写的Shader的数量会急剧上升,比如:

  *.一开始我们希望我们的材质系统能够支持普通光照,我们为这种最简单的效果写一个Shader

  *.然后我们希望这个材质还可以支持贴图,我们为它再写一个Shader,现在有两个Shader了--[支持普通光照,不支持贴图]和[支持普通光照,支持贴图]

  *.然后我们希望为它加上骨骼动画,我们需要为已有的Shader各写一个带骨骼动画的版本,这样Shader的个数就变成4个了,分别是:

[支持普通光照,不支持贴图,不支持骨骼动画]

[支持普通光照,支持贴图,支持骨骼动画]

[支持普通光照,支持贴图,不支持骨骼动画]

[支持普通光照,不支持贴图,支持骨骼动画]

  *.然后我们希望能够加上雾,我们需要为已有的Shader各写一个支持雾的版本,这样Shader的个数就变成8个了,分别是:

[支持普通光照,不支持贴图,不支持骨骼动画,不支持雾]

[支持普通光照,支持贴图,支持骨骼动画,不支持雾]

[支持普通光照,支持贴图,不支持骨骼动画,不支持雾]

[支持普通光照,不支持贴图,支持骨骼动画,不支持雾]

[支持普通光照,不支持贴图,不支持骨骼动画,支持雾]

[支持普通光照,支持贴图,支持骨骼动画,支持雾]

[支持普通光照,支持贴图,不支持骨骼动画,支持雾]

[支持普通光照,不支持贴图,支持骨骼动画,支持雾]

  你可以注意到,当我们每加入一种新的功能的时候,Shader的个数就要翻倍.当我们还要不停的加入新功能时,这个数字很快就会上升到不能容忍的地步了.手工去写这每一个Shader是会让人崩溃的.

 

  有一种解决方法是,我们可以写一个什么都支持的Shader,[支持普通光照,支持贴图,支持骨骼动画,支持雾],然后通过设入不同的参数来达到正确的表现效果,比如我们不需要贴图,我们可以设入一张白色的贴图,如果不需要骨骼动画,我们可以设入一个identity的矩阵数组,如果不需要雾,我们可以把雾的起始距离设到一个很大的值.这样我们只需要写一个功能强大的单一Shader就可以了,但是上面说过,Shader代码有种种限制,这样一个单一Shader随着功能越加越多,会变得非常缓慢,如果这个缓慢的shader的确完成了许多功能,那也可以接受,但当它完成的只是全部功能的一个很小的子集而仍然要耗费同等的时间时,这就无法接受了.据说新一代的显卡和runtime在这方面做了很大的提升,不过我没有用过,不知道会怎么样,我觉得仍然够呛.不管怎么样,我们的engine是面向dx9的,必须寻找另一种方法来解决这个问题.

 

  使用预定义的宏可以比较好的解决这个问题,我们可以写一个长长的容纳各种功能的Shader文件,然后在里面用大量#ifdef/#else/#endif来将各个功能分隔开来,然后在使用Shader时,我们通过指定不同的预定宏的组合来编译这个Shader文件,以得到一个功能被正确裁剪的Shader.既然我们无法在Shader内部里完成动态分支,我们只能让编译器帮我们把各个分支组合编译成一个个静态的Shader了.

 

  在我们的engine里,使用了非常类似的方法,下面介绍一下具体的实现:

  首先要说说我对Shader计算的理解.Shader代码虽然有种种恶劣的品质,但有一点比较好,就是它的最终计算结果是简单的,大多数情况下只是一个float4的颜色而已,这个最终结果可以由一些中间结果计算得到,而这些中间结果又可以由更次一级的中间结果计算得到,我把最终的计算结果称为Output,中间结果称为Factor,计算过程称为Formular,那么一个典型的shader可以用下面的形式描述出来

 

  下面说说具体的实现:

  *.首先我们有一个Shader Library Project的概念,有点类似于vc的一个project,它由若干个文件组成,它的Build结果是一个Shader库.

   

  *.一个Shader库不是一个shader,它包含了很多各种功能的Shader,我们可以根据各种功能的组合,从这个库中生成对应的Shader.

  *.一个Shader Library Project里必须并且仅能包含一个模板文件,模板文件中定义了这个库中会用到的所有Factor(注意是所有的Factor),以及一些缺省的Formula,如下图:

  注意这张图里没有包含ShaderConstant,每个Formula都可以自由的访问所有的ShaderConstant,所以我就不把它画出来了

 

  *.除了模板以外,项目里还包括若干个功能(Feature)文件.一个Feature代表了Shader要实现的一种功能,比如上面说的支持贴图,支持骨骼动画等,一个Feature里面定义了一个或多个Formula,它们都是模板文件里定义过的Formula的重载版本.比如:

 

  功能DirLight重载了一个formula :

   

  

  功能DiffuseMap重载了一个formula:

   

 

  功能Bones 重载了两个formula:

   

 

  功能Fog重载了两个formula

   

 

  *.有了模板文件和功能文件,我们就可以来组合Shader了,首先我们指定我们需要的功能组合,然后我们只需要简单的将功能文件重载的formula替换模板文件里的对应版本就行了,我们会得到一个新的计算流程,如下图:

  

                        

  然后我们根据这个流程来生成对应的shader代码,交给d3dx编译(目前我们使用d3dx的effect系统来使用shader),就可以得到一个组合后的shader了

 

 

  一些说明:

  *. 上面的图形化表示方式其实只是一个示意图,方便读者理解,目前engine中所有的模板文件和Feature文件都是用脚本写的,对它们的处理牵涉到很多语法分析和字符串拼接的工作,并没有一个专门的图形化编辑器,我希望将来有机会能更新到一个真正的图形化的编辑方式.

  *. 出于一些性能上的考虑,目前所有的Feature都定义在C++头文件里的,每一个Feature用一位表示,我们用一个unsigned int64来表示各种Feature的组合.也就是说我们最多可以定义64种不同的Feature,(在当初设计这套系统的时候还是觉得足够的,不过目前有用完的趋势,需要一些trick来处理这个问题.)

  *. 有一些Feature是不能共存的,比如目前关于蒙皮的Feature有四个:Bones1,Bones2,Bones3,Bones4,分别对应于同一顶点受不同骨骼数量影响的情况,它们是不能共存的,我们在模板文件里添加了Conflict Group的支持,可以把彼此互斥的Feature放到同一个Conflict Group中,我们在组合Shader的时候会根据ConflictGroup来检测Feature冲突的情况.

  *. 可能会出现多个可以共存的Feature重载了同一个Formula的情况,我们通过Feature在重载Formula的时候需要指定一个优先级的方法来解决这个问题,多个Feature通过这个优先级来竞争同一个Formula.

  *.使用这套系统只是在一定程度上方便了写shader的过程,设计一个模板文件仍然是很复杂的工作,需要对shader编程有足够的经验,以及总体把握的能力.

 

  使用这套系统来进行Shader的动态组装在本质上和使用#ifdef/#else/#endif并没有区别,但它有一些好处:

  *.精简的主流程,模板文件里记录的只是整个计算流程里的一些关键节点,它不会变得很长,使用#ifdef/#else/#endif很容易导致写一个超大的文件,而主的计算流程会被淹没在各种细节中

  *.同一个Feature的代码可以集中写在同一个文件里,而不用分散在一个大文件的各个地方,易于维护

  *.Feature文件可以被多个Shader项目共用,只要这些项目的模板里定义了相同意义的Formula.

  *.由于Shader的组合是由我们自己完成的,我们可以把组合的结果(一个标准的hlsl文件)展示给用户,便于检查.我不知道d3dx提供的hlsl编译器是不是有类似的功能.

 

  Shader的动态组合包括hlsl代码的组合(这部分由我们完成)以及hlsl代码的编译(这部分由d3dx完成),是比较慢的过程,我们显然不能在每次使用某个shader时都去重复这一个过程,我们会把每一个组合好的Shader都保存在一个cache中,然后用Shader库的名称+64位的Feature组合码作为hash key来索引它.这个cache会被保存在一个文件中,每次游戏运行时会进行加载.

 

  关于Shader的动态组合就说到这,Shader的具体使用上还有些乱七八糟的事,这个下回再说.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值