从Demo到Engine(五) Design a Material System part2

从Demo到Engine(五) -- Design a Material System part2

仅供个人学习使用,请勿转载,勿用于任何商业用途

作者:clayman

 

上一篇文章介绍了基本的材质系统接口,本文将继续讨论一些细节问题。

冗余过滤

         减少状态变化是任何实时渲染系统提高性能的重要方法之一。有人认为如果每次参数更新都执行冗余检查,反而会牺牲性能,还有人觉得驱动会做冗余检查,不必多此一举。有没有必要做冗余过滤呢,答案是肯定的。首先,驱动进行的冗余检查是有限的,其次,如果能在系统的高层就找出冗余的设置,又何必一直传递到最底层再过滤呢?早期检查可以减少DX函数的调用,减少DX函数到驱动的调用,而代价往往只是一次条件判断语句,何乐而不为?

冗余过滤可以在不同层次级别进行,render queue sort就是最高级的冗余过滤,在上一篇文章最后展示的代码中,我们见到了per material级别的过滤,再进一步,还可以做per parameter group或者per parameter级别的过滤。Parameter group是什么概念呢,实际上就是作为一个集合的一系列参数。DX 10下,render state object就是per group过滤的最好例子。DX9虽然没有state object的概念,但自己写一个类似的wrapper也并非难事,XNA 4.0就这么做了!至于Per parameter级别的过滤,应该依据参数类型决定是否需要做冗余检查。比如对基于float类型的变量就很难做冗余判断:

if ( value != cachedFloatVale)

         updateParam();

 

         众所周知,由于浮点数本身的不精确性,这样的判断通常达不到预期效果。特别对于matrix这类包含多元素且易变的值,做冗余过滤显然得不偿失。但对纹理而言,类似的过滤则比较适合。冗余检测的目的不是要100%过滤所有重复设置,只要保证能避免大部分重复操作即可。

 

         实现冗余过滤,需要保存内部系统各变量的当前状态,包括material, shader program, state blocktexture等等。为保证状态一直性,必须让高层系统只能从一个地方修改这些参数。试想如果同时暴露了DeviceEffect,两者都会改变设备状态,那么状态追踪就会变的很麻烦。这里还要说说DX9Effect的第三个缺点:无法获得Effect保存的所有数据,比如sampler staterender state。在.fx文件里直接设置render state看起来是很便利的特性,但对引擎非常不友好,我们无法知道effect改变了哪些状态以及相应的值,相当于失去了对渲染系统的控制。.fx文件里一个错误的状态变量也许会破坏整个场景的渲染效果,而在引擎层次上根本无法找出哪里出错!DX9下,唯一可以解决这个问题的方案就是实现effect state manager。不过Effect已经够慢了,再加一个manager layer岂不是更慢,况且既然都有实现EffectStateManager的勇气了,何不更进一步抛弃Effect,直接管理Shader呢,毕竟编写2者的工作量已经差不多了。

重新发明轮子

         为了避免Effect的种种缺陷,根本的解决方案就是编写自定义Effect/ Meta-Material。这一步其实并没有大多数人想的那么复杂,要做的只是加载/创建shader和管理参数。创建shader把原来的CompileEffect改变为CompileShader再稍做修改即可。注意,meta-material仍然需要有类似techniquepass的概念。DX SDKHLSLWithoutFX介绍的通过ConstantTable管理参数是最容易实现的方法,几乎不需要编写太多额外代码。如果你足够疯狂,可以连ConstantTable都不用,解析出ConstantTable中各变量所对应的寄存器编号和类型,直接SetShaderConstant。这里不仔细讨论如何解析constant table,但有一点需要注意,无论ConstantTable还是SetShaderConstantvs/ps所对应的变量是独立的:比如在.fx文件中声明了全局变量var,并且在vsps中都用到了这个变量,那么此变量对vs/ps来说,分别放在2个不同寄存器中。

一些可能的优化方案

         延迟更新:所有参数,状态更新延迟到DP调用之前再进行,类似EffectCommitChange机制,Material.Apply等操作并不立即设置更新状态,而是标记,记录要进行的操作,DP之前,渲染系统检查是否有需要执行的操作并执行。

         参数打包:把需要更新的参数打包为一个或多个float[],用SetShaderConstate(float[])一次更新多个参数。这点对更新shader中的strut型变量特别有用,比如light,可编写类似SetShaderConstant( lightSemantic, light.ConverntToFloatArray) 的代码。

         参数寄存器绑定:材质/渲染系统优化很大程度上依赖于具体项目和编写shader的方式。对引擎来说,很难要求用户必须以某种特定规则编写shader,但对具体项目来说则是可行的。参数寄存器绑定指的就是所有shader都把相同的参数绑定到同一寄存器,比如color map—s0, normap map – register(s1), world matrix – register (c0),等等。这样不但可以减少冗余设置,也可以简化SetShaderParam时的代码逻辑。另外提一下,尽量不要使用effectPooleffectBlock这类东西,ms在设计整个Effect框架时虽然引入了很多不错的概念,可惜具体实现都不太好,往往得不偿失。

         从更高的层次来看,材质系统是整个渲染系统的一部分,脱离于渲染系统讨论是不完整的。仅靠material中的数据,还不足以渲染物体,worldviewprojection matrix,雾化,灯光参数也必不可少。这些数据由IRenderable或渲染系统的其他部分提供,但都必须通过material接口进行设置。此外,渲染系统还可以保存一些全局material”。物体并不局限于某一个material,在renderer中,为了实现某些全局效果,可以用renderer中定义的材质替换原来的material,比如希望做z-pre pass

Renderer.EnableZPrePass = true;
................
if (EnableZPrePass)
     render all objects with z
-pre pass material
else
     render all objects with normal material

         这样设计的优点是添加,修改全局效果时,只需要做最小的改动,而不必修改所有material。上一篇文章的评论中,有人提到了对Deferred Rendering的支持,也是类似的思路。无论forward还是deferred shading,材质系统并不需要做出改变,只需让shader代码支持相应的渲染方式,并在渲染器中做出调整就可以了,比如:
deferredShading
meta-material { technique01: normal, technique02, deferred , other technique…….};
meta
-material.SetTechnique( currentGlobalShadingProfile);
…………………
Renderer:
if( deferredShading)
{
  render all objects with per 
object’s normal material(ds technique) //geometry phase
  perform screen-space lighting with renderer specific material
  perform composition with renderer specific material
  post
-process with renderer specific material
}
else
........

         That’s All! 关于材质系统,最重要的设计理念就是抽象出一个统一的机制,可以管理任意shader,处理任何类似的参数,同时保证性能。这两篇文章并不完整,只介绍了基本思路和粗略的实现方法,multi-materialmulti-pass等概念都没有讨论。希望能对正在设计引擎的同学有所帮助,也欢迎高手指正不足之处。


ps:不出意外的话,偶的<<Game Engine Architecture>>这周应该到了(保佑邮递员叔叔不要看错e文地址),我会参考其中的内容,继续这个系列的文章:)

 

原文地址:https://www.cnblogs.com/clayman/p/1732211.html