• 从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();
    }

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

  • 相关阅读:
    Convert between Unix and Windows text files
    learning Perl:91行有啥用? 88 print " ----------------------------------_matching_multiple-line_text-------------------------- "; 91 my $lines = join '', <FILE>;
    Perl:只是把“^”作为匹配的单字:只是匹配每一行的开头 $lines =~ s/^/file_4_ex_ch7.txt: /gm;
    Perl: print @globbing." "; 和 print @globbing; 不一样,一个已经转换为数组元素个数了
    为什么wget只下载某些网站的index.html? wget --random-wait -r -p -e robots=off -U mozilla http://www.example.com wget 下载整个网站,或者特定目录
    正则表达式中 /s 可以帮助“.”匹配所有的字符,包括换行,从而实现【dD】的功能
     是单词边界锚点 word-boundary anchor,一个“”匹配一个单词的一端,两个“”匹配一个单词的头尾两端
    LeetCode103 Binary Tree Zigzag Level Order Traversal
    LeetCode100 Same Tree
    LeetCode87 Scramble String
  • 原文地址:https://www.cnblogs.com/clayman/p/1728502.html
Copyright © 2020-2023  润新知