IxEngine开发笔记

导航

第十二回 关于Shader和材质

这回的内容估计比较杂乱一些,因为都是些零碎的东西.

上回关于Shader组合的文章发到论坛上后,很多人都说可以用静态分支的方式来解决Shader组合爆炸的问题,比较让我惭愧的是,那时候我连静态分支是什么都不知道.Google了一下才知道,所谓的静态分支原来是指shader在正式运行前,可以把该怎么分支预先计算出来,然后在所有的后续计算中都统一按照这种分支来走计算流程.好像要SM3.0以上才能支持.我觉得这的确是一种不错的技术,不过对我来说可能用起来会比较吃力一点,因为我不知道静态分支在各种各样的显卡上的表现究竟怎么样,虽然SM 3.0现在也蛮普遍了,不过也许未必所有的显卡都能支持的很好吧,而且在代码优化上可能有些讲究,这些都需要对各种显卡有比较深入广泛的研究,而这方面都是我比较欠缺的.而且如果完全用静态分支来干的话,随着功能不断的添加,最终可能会写一个越来越长的shader,大到甚至超过指令限制,到时候又不得不拆成多个小shader,我的感觉是扩充性不好,对性能的控制也不方便.何况现在已经有一套shader组合的系统了,用着也蛮顺手,就先不考虑这个东西了.也许将来升级到更新的DX版本时可以考虑.

关于Shader还有些东西可以介绍.首先我觉得写一个Shader编写的工具还是有必要的,尤其是当你有一套自己的Shader系统的时候.因为这样可以比较方便的定制各种功能.我写了一个,我叫它ShaderComposer.看上去象这样:

这个简陋的IDE有以下功能:
*.可以打开一个工程文件,工程文件记录了一些模板文件,Feature文件和Unique Feature文件(Unique Feature下面会提到)
*.提供支持语法高亮的文本编辑器编辑这些文件,可以打开多窗口,文本编辑器用Scintilla实现
*.Validate功能,可以设定不同的Feature组合,组合成一个hlsl文件,并编译它,如果有错误可以把它输出在Output窗口中.点击Output的错误信息可以定位到发生错误的位置.
*.组合后的hlsl文件以及编译后的汇编代码可以显示在一个窗口中.
*.也有Validate所有Feature组合的功能,不过这个功能不太实用,因为到后来组合会非常多,可能有十几万种,Validate一遍太慢了,没有勇气用.
*.最后所有的模板,Feature,UniqueFeature会以二进制文件被保存到一个指定的文件里,这个文件就是所谓的Shader库了.注意Shader库里不包含任何编译结果,
上面说到的Unique Feature其实就是一个用hlsl写的完整的shader,因为有些shader没有那么多的功能组合,典型的比如说一些后处理特效,这样只要写一个单一的hlsl文件就行了,不需要再搞一套template/feature的机制了.放在Shader库里只是为了方便管理

   这个编辑器也有年头了,现在看着越来越不爽,需要改进的地方:

   *.要增加版本控制,可以check in/check out一些文件

   *.由于有很多共用的feature,当修改某个feature后,往往要挨个打开所有的库,一个一个重新编译,这很不方便,最好有一键编译所有库的功能.

   *.脚本文件(尤其是template文件)不直观,要有图形化的界面

   *.想到再加...

   希望有机会可以更新到一个完全图形化界面的ShaderComposer2. 

 

前面一篇文章介绍过,Shader是一种资源,一个Shader库可以提供很多很多的Shader,Shader库是支持热加载的,当我们更新了一个Shader库后,所有来自于这个Shader库的Shader都会被重新载入.这大大方便了Shader的调试.通常我的调试方法是在ShaderComposer里修改shader代码,然后F7编译,然后切换到编辑器窗口查看显示效果,再切回ShaderComposer继续修改,直到效果正确为止.我觉得这种方式很方便.再次推荐Shader资源的热加载.

然后是关于Shader的参数设置的问题.由于我们使用了D3DX提供的Effect系统来使用shader,所以我叫这些参数EffectParam,出于性能考虑,我在一个头文件里定义了所有Shader会用到的参数,如下:
enum EffectParam
{
EP_None=-1,

EP_colDif,
EP_colSpec,
EP_shiness,
EP_shinestr,
EP_diffusemap,
EP_specmap,
EP_world,
EP_view,
EP_proj,
...
...
//General use params
EPG_f_01,
EPG_f_02,

EPG_fx3_01,
EPG_fx3_02,

EPG_fx4_01,
EPG_fx4_02,

...
...
EffectParam_Max
};

然后会在cpp文件里填写一张有EffectParam_Max项的表,来为EffectParam里的每个值指定一系列相关的信息,包括它的hlsl中的名称,显示名称,数据类型,语义,数组大小,一些标志,动画类型等,这些信息大多是为了编辑,比如EP_shiness的对应信息有:
"ep_shiness": 一个字符串,hlsl文件中这个参数的名称
"高光系数":一个字符串,用来在编辑时显示给用户看
GVT_Float:数据类型,一个浮点数
GSem_Shiness:Shiness的语义,我们根据这个语义选择适合的编辑方式,
1:数组大小为1
EPFlag_MtrlEdit: 一个标志位,表示这个param可以在材质中编辑

每个Shader资源都维护一个EffectParam_Max大小的D3DXHANDLE数组,用来记录每一个shader中可能有的参数句柄,当一个Shader资源载入时,首先当然是根据Feature组合从ShaderLib中得到一段组合后的hlsl代码,然后交给D3DX编译,编译通过后,我们会根据上面这张表的参数名称项,为每一个EffectParam到编译好的shader中查询参数的句柄,如果找到了,就把它保存起来,如果找不到,则标记为NULL.这样我们就可以利用这个D3DX_HANDLE的数组来快速的设置Shader的参数了.一些接口如下:

enum EPResult
{
EPFail=-1,//彻底的失败,这个参数没有被成功的设到shader对应的ep里,会影响渲染的结果
EPMissing=0,//shader里不存在这个EP,一般不影响渲染
EPOk=TRUE,
};


class IShader:public IResource
{
public:
virtual BOOL ExistEP(EffectParam ep)=0;//Check whether the shader need this effect param to work

//general SetEP functions
virtual EPResult SetEP(EffectParam ep,i_math::vector3df &v)=0;
virtual EPResult SetEP(EffectParam ep,i_math::vector2df &v)=0;
virtual EPResult SetEP(EffectParam ep,ITexture *tex)=0;
virtual EPResult SetEP(EffectParam ep,i_math::f32 f)=0;
virtual EPResult SetEP(EffectParam ep,i_math::s32 s)=0;
...
};
使用这种方式有一个不好的地方是,参数的种类会非常多,会导致shader中D3DX_HANDLE的数组非常大,并且稀疏.所以我们在这里定义了一些通用参数(EPG_XXX),一些shader的比较特殊的参数可以通过它们来设置,而只有(在各shader中)最通用的参数才会有意义明确的定义和编辑方式.尽管如此,这个数组也不小了,有接近90个元素,不过尚在可以接受的范围.

关于Shader就说到这里,下面说说和shader关系很密切的材质.
材质也是一种资源,它包含的内容可以从它的编辑面板中清楚的看到:

包含的内容:
*.最重要的当然是shader库的名称了.(红框)
*.然后是一些Feature,这些Feature只是一些物体本身的显示效果,并不牵涉环境因素(比如光照,雾)或者模型属性(比如骨骼动画) (蓝框)
*.然后就是各种参数了,我们会根据这个材质需要的Feature生成一个shader,并找出这个shader所需要的参数,把它们加到属性框中,供用户编辑.并且材质还为这些参数提供了动画功能,以实现一些变化的效果(当然基本上是周期性的). (绿框)
*.最后是和shader没关系的渲染状态,也就是SetRenderState(..)所对应的那些,我们把常用的一些状态分为几个组,每个组有若干个选项,分别对应一套RenderState的设置.这样可以简化用户的设定. (黄框)

最后说一下材质树.
材质树绝对是很好的编辑方式,你当然可以通过添加Shader代码的方式增加材质效果,但这样做的环节太多,很不方便.而且使用图形化的方式更为直观,我在实际使用中确实感到有这方面的需求.不过我一直担心它会造成Shader数量过多的问题,以前我考虑解决这个问题的方式是想法子合并多个材质树到一个单一的shader,不过最近我又想了一下,也许可以这样,材质树可能只是作为模板存在,它的数量将不会很多,每棵材质树可以暴露出多个参数,而在真正的材质中可以指定使用的材质树模板,并且可以设定这棵材质树暴露出来的参数.我估计Unreal的material instance大概也就是这个意思吧.初步的想法是可能需要写一个专门的Shader的template,并指定几个Factor专门由材质树重载.但也会有自定义参数的问题.对现有架构应该有不小的影响,具体还没有想得很清楚,不过这个功能一定会加,希望能够早日完成.
下回说说其它一些资源.

posted on 2010-08-25 22:07  ixnehc  阅读(3213)  评论(3编辑  收藏  举报