zoukankan      html  css  js  c++  java
  • 从Demo到Engine(四) Design a Material System part1

          从Demo到Engine(四) -- Design a Material System part1

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

    作者:clayman

     

    前言:质,材质,材质,终于写到这个主题了。原以为一篇可以写完,结果写着写着就到2,3篇的篇幅了。与这系列的前几篇不同,文章中大部分设计和构想,都是我自己总结出的方案,并且还在不断改进中,难免有欠缺不妥之处,仅作为参考。文中所有代码均为伪代码。

     

        在讲什么是材质系统之前,先看看什么不是材质系统。在论坛里看到过大量个人或者小团队写的引擎,宣称有强大的材质系统,实现了各种花哨的特效,再仔细看代码,每种特效都对应了一个特别的class。那么这就显然不是材质系统,更像一个材质库。对,材质系统不等于材质库。材质系统的目的是管理“所有”材质,为所有材质提供一个统一的容器。可惜人们常常把两者混为一谈,甚至本末倒置,开发引擎时把大量精力用于编写特效,却没有提供一个可扩展的框架。在我看来,特效甚至不属于引擎开发的范畴,而是游戏内容的一部分。那么材质系统应该是什么样呢?很简单,如果能用类似如下的伪代码管理所有特效,那么你就完成了一个材质系统:

     

    Materia mat = LoadMaterial(materialName); //create material
    mat.SetShaderParameter(); //update material parameter
    mat.Apply(); //set material to device

         嘿,这不是和DX里的Effect差不多吗?确实如此,因为两者都是以相同的目标来设计的。那为什么不直接使用Effect呢(这也是论坛里最常见的问题)。在我看来就算不考虑跨平台,Effect也有2个最重大的缺陷:

     

    1. DX里的Effect既负责参数的管理更新,同时也是材质参数的容器。这显然违反了OO里的单一职责原则,带来的结果就是大大降低灵活性,这一点下面会讲到。

    2. DX9Effect更新参数的效率*非常,非常*低下。首先,Effect属于DX的扩展库D3DX,内部调用的仍然是SetXXXShaderSetXXXConstant这样的函数,并没有什么神奇之处。其次EffectPass.Begin会重新更新Effect中所记录的所有参数和渲染状态,不做任何冗余过滤。最后,CommitChange会重新更新某些没有发生变化的参数。处于性能考虑,任何有经验的开发人员都不会建议你使用Effect更新参数。DX10后续的版本中,由于引入了StateBlockConstantBuffer等概念,情况要好很多。

     

             DX里的Effect完全就不可一用吗?当然不是,对于中小型项目来说,Effect仍然是一个很好的选择,可以节约大量开发时间。但对于引擎来说,就有几分力不从心了。如何编写一个更好的材质系统呢?从上面的伪代码来看,似乎很简单,但魔鬼尽在细节之中。

     

             材质到底是什么呢?任何会引起物体表面视觉效果改变的属性都是材质的一部分,具体从程序角度来看,分为两部分:材质参数(颜色,反射折射率,纹理等等)以及Shader程序。知道了这一点,再看Effect的第一个缺陷,就更明显,他把两者紧密的绑定为一体,是一一对应的关系。但在实际应用中,对具有相同材质属性的物体,我们可以选择不同的光照模型(shader)来渲染,是一对多的关系,参数可以独立于shader存在。此外,参数是可变,易变的,是由上层游戏物体来决定的属性,不应该使用一个非常底层的类来储存。虽然用Effect也可以完成类似功能,但代码会非常丑陋,此外,把底层类暴露给上层结构也是非常不明智的选择。

     

             再说一点关于材质创建 (主要是shader)的问题。 主流观点有两种,一种是传统的靠程序员全手写,另一种则是编写类似于MayaHyperShaderShading Tree系统,允许非专业程序员通过拖放节点,自动生成。显然,第二种方式看起来很吸引人,但我更倾向于前者,前段时间还在某论坛就这个问题和人进行了一些了讨论J。后者的优点并不是没有代价,编写这样一个系统非常复杂,很难在既保证灵活性的前提下又兼顾性能。更关键的问题是你需要一个自动化系统吗?对于Maya来说,主要用户是非程序员,因此,一个图形化创建系统是必须的。但对游戏开发来说,虽然有引擎宣称不懂编程也可以做游戏,大多是个噱头。特定游戏里会用到多少shader呢?不考虑变体的情况下,上百个已经是*非常*多了。考虑到shader程序通常不会太长,这个数量级,有经验的开发者编写起来并不会太费时间。对于各种shader变体,复制粘贴的方法也未尝不可。好了,用一周时间可以写完所有shader的情况下,是否值得花一个月时间编写一个你不知道是否可用的工具呢?更不要说以目前的水平,自动生成的代码完全不可能有手动优化的效率高。当然,由于DX11对动态生成shader有更好的支持,后者也许是未来的趋势。

     

             回到材质系统的设计上来,前面说过应该分离参数和shader,最直接的设计就是创建两个类:

    Material
    {
        
    params;
        metaMaterial;
    }
    Meta
    -Material
    {
        defaultParams
        ShaderProgram;
    }

                  Material储存每个物体不同的属性,和物体是一一对应的关系,也可以多个物体对应一个。Meta-material则主要用于保存shader以及默认参数,因此每种不同的meta-material只应该有一个实例。注意,以下类型都属于材质参数:数值(float, int, vector,matrixarray…..),布尔值,纹理,render statesampler state。用这2类,可以编写类似的渲染代码:

    MetaMaterial.Apply()  //set shader, set default parameter
    foreach obj
    {
       material.Apply()  
    //set per object parameter
       DrawPrimitive();
    }

     

    简单吗?不,细节来了!Material.params应该保存哪些参数?Sampler staterender state(个别状态除外)通常来说属于shader属性,没必要保存在material中。Material最好只保存与物体表面属性相关的参数。由于可以随时改变meta-material,可能出现参数不匹配的情况,metaMaterial不一定用到material中的所有参数,也可能需要一些material中未定义的参数。如何实现Material.Apply()呢?

    Material.Apply
    Material.Apply()
    {
        
    foreach parameter in material.paramCollection
            material.metaMaterial.TrySetValue( semantic, parameter);
    }

    Material.Apply()
    {
        
    foreach shaderParam in material.metaMaterial.shaderParamCollection
            shaderParam.SetValue(material.paramCollection.TryGetVale(shaderParam.semantic);
    }

    上面2种实现,一个采用push模型,一个采用pull模型,哪一种要好些?如果paramCollection中仅保存与defaultParam中不同的值,那么显然前者要好。但仅保存差异数据,会有一些副作用,比如:

    defaultParam{ a = value1, b= value2};

    Mat1{a = value3};

    Mat2{b = value4};

         如果先渲染mat1,那么渲染mat2时,默认的a值已经改变为了value3,但mat2仍然以为是value1 一种可能的解决方案是添加PostRender函数:

    MetaMaterial.Apply()  //set shader, set default parameter
    foreach obj
    {
        material.Apply();  
    //set per object parameter
        DrawPrimitive();
        matrial.PostRender();
    }

      把修改过的参数恢复为默认值,但这也增加了程序代价。此外,还有第二个副作用,由于可以在任何时间修改替换metaMaterial,不同的metaMaterial会有不同的默认参数。解决这个问题,需要修改原来的mateial定义:

    代码
    Material
    {
        paramModifier;
        MaterialTemplete;
    }
    MaterialTemplete
    {
        readOnlyDefaultParam;
    }
    MetaMaterial
    {
        shaderParams;
        shaderProgram;
    }

    MetaMaterial.Apply()  
    foreach obj
    {
      
    if(currentMaterialTemplete != material.materialTemple)
          materialTemple.Apply()

      material.Apply();
      DrawPrimitive();
      matrial.PostRender();
    }

    未完待续,下一篇会展开讨论更多实现细节和性能优化........

  • 相关阅读:
    uva 10369 Arctic Network
    uvalive 5834 Genghis Khan The Conqueror
    uvalive 4848 Tour Belt
    uvalive 4960 Sensor Network
    codeforces 798c Mike And Gcd Problem
    codeforces 796c Bank Hacking
    codeforces 768c Jon Snow And His Favourite Number
    hdu 1114 Piggy-Bank
    poj 1276 Cash Machine
    bzoj 2423 最长公共子序列
  • 原文地址:https://www.cnblogs.com/clayman/p/1728502.html
Copyright © 2011-2022 走看看