# 效果和材质

材质(Material)是一种用于描述物体如何被绘制的抽象,其主要目的是为艺术家提供一个界面,艺术家可以使用这个界面修改物体的表面属性,例如颜色、纹理、粗糙度等,而渲染器则可以利用这些数据忠实还原艺术家的动机。

为了使得材质能够更加通用,小游戏框架抽象了另一个名为“效果(Effect)”的概念,其可以认为是一个材质模板,通过**光照模式lightMode宏定义definition**来对模板的各个功能进行开关。

# 效果-Effect

在真正的渲染过程中,物体的渲染实际上是由前面章节提到的着色器,也提过了着色器中可以用光照模式和宏定义来区分不同的分支,而效果,即engine.Effect,则是基于着色器并且加上了一些其他可以管理渲染效果的管理器。

# 创建Effect

在着色器一节,我们实际上已经创建了一个Effect,但当时着重于描述着色器本身,在编写完shader后,就可以来关注.effect文件本身了,将这个文件用编辑器打开,可见:

{
  "shaderProperties": [
    {
      "key": "_Color",
      "type": "Vector4",
      "default": [
        1,
        1,
        1,
        1
      ]
    }
  ],
  "textures": [
    {
      "key": "_MainTex",
      "type": "Texture2D",
      "default": "white"
    }
  ],
  "defaultRenderQueue": 2000,
  "passes": [
    {
      "lightMode": "ForwardBase",
      "vs": "./custom.vertex.hlsl",
      "ps": "./custom.pixel.hlsl",
      "multiCompile": [
        [
          "USE_INSTANCING",
          "__"
        ],
        [
          "USE_FEATURE_A",
          "USE_FEATURE_B"
        ]
      ],
      "compileFlags": ["Fog"],
      "useMaterialRenderStates": true,
      "renderStates": {
        "blendOn": false
      }
    }
  ],
  "editorInfo": {
    "assetVersion": 2
  }
}

显而易见,这其实就是一个json,开发者可以编辑里面的内容来定制一个Effect。

# 配置Passes

首先是passes,其定义了有这个效果有多少个可渲染的Pass,Pass可以理解为一个渲染的批次,所有进入渲染的物体都可以在一个Pass中按照特定的状态被渲染一遍,如果有多个Pass,那么自然就可以被渲染多边,而决定某个批次的渲染是否要用到某些Pass最重要的就是lightMode

lightMode即光照模式,是一种给开发者区分同一物体将在何种模式下被渲染的参数,内置的有ForwardBaseForwardAddShadowCaster等,有一定图形编程经验的开发者对这些一定不陌生,实际上在实际的渲染驱动中,每一批次的渲染总是针对某一种lightMode的,而在后面要讲到的内置管线中,默认也是会有个ShadowCater -> ForwardBase -> ForwardAdd的顺序。当然,可以有多个Pass使用同一种lightMode,这种情况下,它们都会在同一个批次被渲染。

决定了lightMode,就需要指定该Pass使用的着色器了,着色器分为顶点着色器vs和像素着色器ps,这个在着色器一节已经论述过。

# 配置宏定义

每个Pass中都会有multiCompilecompileFlags,这些都表示了宏定义,它们既会影响到着色器中对应的宏,也会影响到变体的生成。

变体,即着色器在应用了不同的宏开关组合下生成的不同的最终代码,比如这么一段着色器代码:

#ifdef USING_BLACK
    return float4(0, 0, 0, 1);
#else
    return float4(1, 1, 1, 1);
#endif

其有一个宏USING_BLACK,则应用或不应用这个宏将分分别生成return return float4(0, 0, 0, 1);return float4(1, 1, 1, 1);两份代码,这就是两个变体。

了解了何为变体,就要知道如何才能使用宏来生成变体,让我们这个示例Effect里的变体定义:

"multiCompile": [
  [
    "USE_INSTANCING",
    "__"
  ],
  [
    "USE_FEATURE_A",
    "USE_FEATURE_B"
  ]
],
"compileFlags": ["Fog"],

multiCompile用于用户自己定义的宏,每一个元素都是一个数组,数组中的字符串代表哪些组合可以联合生成变体,比如第二个元素中,就定义了USE_FEATURE_AUSE_FEATURE_B可以联合生成变体,而第一个元素中的__则代表USE_INSTANCING这个宏将单独生成变体。

compileFlags则表示小游戏框架的内置变体,比如这里使用了内置的Fog,开启后可以在着色器中使用APPLY_FOG(In, outColor);来应用雾。

注意这里只是配置了某个Pass支持哪个宏定义,而实际的开关则是由全局、物体、材质三方共同决定的,开发者一般只需要控制下面会论述到的材质部分,当然也可能有进阶需求,那么就可以参考渲染器渲染系统的相关部分。

支持的内置变体可见文章末尾的附录。

# 配置渲染状态

对于Pass而言,宏之外最重要的就是renderStatesuseMaterialRenderStates

renderStates顾名思义即渲染状态,渲染状态直接对应底层图形API都会提供的一系列概念,比如混合blend、深度depthTest等,比如示例里就定义了blendOn: false,即表明要关掉混合

所有支持的渲染状态可见文章末尾的附录,这里先不赘述,这里主要要说明一个规则,即useMaterialRenderStates这个参数的作用。

在小游戏框架的设计中,渲染状态是由Effect决定默认值,Material决定最终值的,而useMaterialRenderStates则就是用于决定Material是否能够覆盖Effect默认值的,如果为false,则总是使用Effect提供的默认值,如果为true,则由下面会讲到Material的渲染状态的规则来决定。

# 配置UniformBlock

在Pass之外,Effect的另一部分就是其中的shaderPropertiestextures,它们决定了这个Effect的各个着色器可以使用哪些Material维度的Uniform,而这个基本是对齐着色器中定义的cbuffer Material的,在这个示例中,我们定义了:

"shaderProperties": [
  {
    "key": "_Color",
    "type": "Vector4",
    "default": [
      1,
      1,
      1,
      1
    ]
  }
],
"textures": [
  {
    "key": "_MainTex",
    "type": "Texture2D",
    "default": "white"
  }
],

所有的纹理类型Uniform都写在textures内,其他的则写在shaderProperties内,示例中定义了一个名为_ColorVector4向量,默认值为1,1,1,1,而后定义了一个名为_MainTex的纹理,类型为Texture2D,默认值为内置的white

这个定义在着色器中对应的cbuffer为:

cbuffer Material
{
	float4 _Color;
	float4 _MainTex_ST;
}

DECLARE_TEXTURE(_MainTex);

支持的Uniform类型和可用默认值可见文章末尾的附录。

# 渲染队列

defaultRenderQueue定义,将在下面的Material一节中详细论述。

# 材质-Material

有了Effect,就可以来创建材质Material了,和Effect不同,Material一般是在IDE中创建的:

创建完成后,在右侧的Inspector中可以有一些可供我们配置的属性。

# Effect相关

首先是Effect,之前说过可以认为Effect是一种材质模板,那么Material就可以认为是Effect的一个实例,所以首先要给其指定使用的Effect:

在这里面的Effect一栏,我们可以选择需要使用的Effect,如果是要选择自己创建的,则要选择Local Effect,然后在弹出的窗口中选择,这里我选择了方才创建的custom.effect

如此,Material便使用了方才的Effect。在设置好了Effect后,还有其他的两个选项renderQueueuseInstance

renderQueue即渲染顺序,决定了使用了这个Material的物体将在什么顺序被渲染,其值越大顺序越往后。这个值默认来自于Effect的defaultRenderQueue

注意,约定renderQueue >= 2500的物体为透明物体,这将影响到物体按照深度排序的规则。

useInstance这个参数则是决定了使用该Material的物体是否能够开启GPU实例化。

GPU实例化(Instancing)是一种用于提升渲染性能的技术,其可以一批绘制大量物体,但要求物体的Mesh和Material均“完全一致”。

勾选useInstance是简单的,但并不代表物体能够真的被实例化,需要满足很多约束条件,详见GPU实例化

除了自定义Effect,小游戏框架还提供了许多内置的Effect,这些内置的Effect可以参考末尾附录。

# 宏定义

在Effect中,我们介绍过可以在Effect中定义一些宏来使用。而其中一部分宏的开启就可以在Material中完成,在Inspector的第二部分,即*Render Definitions一栏:

如图,点击“+”便可以增加开启一个宏,这里我开启了一个宏USE_INSTANCING

# 渲染状态

在宏定义之后是渲染状态:

Material的渲染状态和Effect中的定义完全一致,只不过其是用于覆盖Effect中的默认状态的,如同前面章节所言,这个覆盖只能在Effect开启了useMaterialRenderStates的时候方能生效。

比如这里我就开启了blendOn,并覆盖了原先默认的blendOn: false

# UniformBlock

最后就是针对UniformBlock的配置,也即对应Effect中的shaderPropertiestextures。开发者可以再这里对Effect中设置好的默认值进行初始值的覆盖:

比如在这里,我就将_Color的值覆盖为了1,0,0,1,并且如果想,也可以将_MainTex设置为开发者想设置的贴图。

# 在Runtime中调整

IDE中只能设置Material的初始化状态,而材质中的某些参数在运行时却是可能经常变更的,最常见的就是Uniform,当然渲染状态或者宏也可能会有一些调整。

为了让开发者能够进行方便得修改、获取这些参数,小游戏框架提供了一些方法:

// 获取和修改一个float型变量
material.getFloat(key);
material.setFloat(key, value);

// 获取和修改一个vector型变量
material.getVector(key);
material.setVector(key, value);

// 获取和修改一张贴图
material.getTexture(key);
material.setTexture(key, Texture);

// 获取和修改某个渲染状态
material.getRenderState(key);
material.setRenderState(key);

# 附录

# 内置宏

内置的宏和对应在着色器内的使用方法,请见着色器中对应的章节。

# 渲染状态

内置的渲染状态以及类型如下,其中所有的枚举值请见API文档

interface IPassRenderStates {
  // blend
  blendOn: boolean;
  blendSrc: engine.EBlendFactor;
  blendDst: engine.EBlendFactor;
  blendFunc: engine.EBlendEquation;
  // cull
  cullOn: boolean;
  cullFace: engine.ECullMode;
  // depth write
  depthWrite: boolean;
  // depth test
  depthTestOn: boolean;
  depthTestComp: engine.ECompareFunc;
  // stencil write
  stencilWriteMask: number;
  // stencil test
  stencilTestOn: boolean;
  stencilRef: number;
  stencilReadMask: number;
  stencilComp: engine.ECompareFunc;
  stencilPass: engine.EStencilOp;
  stencilFail: engine.EStencilOp;
  stencilZFail: engine.EStencilOp;
  // PrimitiveType
  primitiveType: engine.EPrimitiveType;
}

# 内置Uniform类型

在Effect可定义Uniform的类型列表如下:

Float:浮点类型
Vector2:2维浮点数组
Vector3:3维浮点类型
Vector4:4维浮点类型
Texture2D:2D纹理,可设默认值为“white”、“black”、“red”、“yellow”、“green”、“blue”、“transparent”
TextureCube:立方体纹理,可设默认值为“white”、“black”、“transparent”

# 内置Effect

内置的Effect可以分为不同的种类,有3D物体的,有粒子的,还有UI的等等,这些Effect将在不同的特定章节中论述:

3D物体的主要和光照有关,详见光照章节内相关内容。
粒子的详见粒子章节内相关内容。
UI的详见UI章节内相关内容。