Files
netease-modsdk-wiki/docs/mcguide/16-美术/7-材质与着色器/材质配置说明.md
2025-03-17 13:24:39 +08:00

20 KiB
Raw Blame History

front, hard, time
front hard time
入门 分钟

材质配置说明

前言

本文将详细介绍材质文件的结构与配置方式。

加载的材质文件列表

材质文件存放于资源包的materials文件夹下我们打开resource_packs\vanilla\materials目录可看到包含有下面这些文件这些是原生微软定义的材质文件

material_list

而resource_packs\vanilla_netease\materials目录下的材质文件则是网易对微软材质文件的修改与扩充。

下面我们就先以原生微软的材质文件进行讲解,首先,目录下面的文件基本都是以".material"为后缀的文件除此之外还有3个重要的json文件分别是common.jsonfancy.jsonsad.json。

我们先来看看sad.json和fancy.json他们是用于控制画质表现的内部各自定义了一个材质文件列表fancy.json通常比sad.json会多定义几个材质文件和可能会为某些材质文件多添加了一些额外的宏shader中可通过对这些宏的判断做特殊的处理

sad.json:

[
	{"path":"materials/sad.material"},
	{"path":"materials/entity.material"},
	{"path":"materials/terrain.material"},
	{"path":"materials/portal.material"},
	{"path":"materials/barrier.material"},
	{"path":"materials/wireframe.material"}
]

fancy.json:

[
	{"path":"materials/fancy.material", "+defines":["FANCY"]},
	{"path":"materials/entity.material", "+defines":["FANCY"]},
	{"path":"materials/terrain.material", "+defines":["FANCY"]},
	{"path":"materials/hologram.material"},
	{"path":"materials/portal.material", "+defines":["FANCY"]},
	{"path":"materials/barrier.material"},
	{"path":"materials/wireframe.material"}
]

可见fancy.json比sad.json多定义了fancy.material,hologram.material材质文件另外也为多个材质文件定义了FANCY宏。 游戏内 设置/视频/精美贴图 这个开关就是控制sad与fancy的切换。当精美贴图开关打开时fancy.json中的材质文件就会生效而关闭时sad.json的材质文件就会生效

fancy_switch

为了实现更好的表现效果fancy.json中的材质文件通常具有较复杂的运算而sad.json中的材质则通常以牺牲一点渲染表现去换取更好的性能。 开发者若需要编写计算较为复杂shader则建议同时编写一个低消耗的版本然后分别把他们定义在fancy与sad内。让玩家在游戏中通过精美贴图选项自行控制是否开启相应效果。

接下来再看看common.json

[
	{"path":"materials/particles.material"},
	{"path":"materials/shadows.material"},
	{"path":"materials/sky.material"},
	{"path":"materials/ui.material"},
	{"path":"materials/ui3D.material"},
	{"path":"materials/portal.material"},
	{"path":"materials/barrier.material"},
	{"path":"materials/wireframe.material"}
]

相比于sad与fancy可互相切换。common.json中定义的材质文件进入游戏后都将被加载。除了在common.json, sad.json, fancy.json中声明的材质文件其余的材质文件并不会被加载。

材质语法

我们使用其中一个材质文件entity.material来进行讲解打开文件我们可以看到文件是以materials开头然后定义了版本号version为1.0.0,这些都是固定格式,标识了这个材质文件的解析方式,我们可以暂时不理会不修改:

material_head

接下来讲解材质中字段的格式我们用entity.material中定义的第一个材质来看

entity_static

可以看到材质里面每一个字段的定义都是键值对的形式,例如:

[
	"vertexShader": "shaders/entity.vertex",
]

冒号左边代表键为vertexShader,右边代表值为shaders/entity.vertex

另外还有列表形式的定义:

[
	"vertexFields": [
        { "field": "Position" },
        { "field": "Normal" },
        { "field": "UV0" }
    ],
]

用符号[ ]声明是列表然后里面是每个子元素的json定义。

材质所有的属性字段概况

我们把所有字段按功能分到以下几类中:

渲染状态

states

配置渲染环境,可以有以下的值:

EnableAlphaToCoverage 半透明对象顺序无关渲染方式的一种支持MSAA的环境下这个开关才有用开启后物体边缘会根据透明度作更精确的柔和和过渡也可用于有大量网格交错重叠的一些复杂场景。
Wireframe  绘制线框模式
Blending : 开启颜色混合模式常用于渲染半透明对象。声明这个之后通常也需要声明混合因子blendSrc,blendDst

DisableColorWrite  不往颜色缓冲区写入颜色值RGBA通道均不写入
DisableAlphaWrite  不往颜色缓冲区写入透明度alpha值允许写入RGB值
DisableRGBWrite  不往颜色缓冲区写入透明度RGB值允许写入Alpha值

DisableDepthTest  关闭深度测试
DisableDepthWrite  关闭深度写入

DisableCulling : 同时渲染正面和反面
InvertCulling :使用正面裁剪。默认情况下为背面裁剪,声明这个之后则渲染背面,裁剪掉正面

StencilWrite :开启蒙版写入
EnableStencilTest  开启蒙版测试

着色器路径

vertexShader

顶点着色器的路径通常为shaders/XXX.vertex。

vrGeometryShader 或 geometryShader

几何着色器的路径通常为shaders/XXX.geometry移动端用不到无须考虑修改。

fragmentShader

片段着色器的路径通常为shaders/XXX.fragment。

Shader宏定义

defines

为使用到的Shader定义宏。为了代码的复用我们很多不同的材质会使用相同的shader。此时若想shader里面某处根据当前材质执行不一样的逻辑则可以通过材质defines声明的宏去做判断。 我们可以用下面entity_for_skeleton这个材质做说明这里可以看出定义了USE_SKINNINGUSE_OVERLAYNETEASE_SKINNING三个宏。

"entity_for_skeleton": {
	"vertexShader": "shaders/entity.vertex",
	"vrGeometryShader": "shaders/entity.geometry",
	"fragmentShader": "shaders/entity.fragment",
	"+defines": [ "USE_SKINNING", "USE_OVERLAY", "NETEASE_SKINNING" ],
	"vertexFields": [
		{ "field": "Position" },
		{ "field": "Normal" },
		{ "field": "BoneId0" },
		{ "field": "UV0" }
	],
	"msaaSupport": "Both",
	"+samplerStates": [
		{
			"samplerIndex": 0,
			"textureFilter": "Point"
		}
	]
},

再看顶点着色器entity.vertex中会有通过#ifdef#else#endif来对宏进行判断并执行不同的逻辑分支宏的这些判断语句是编译期处理的不像传统shader中的if else编译期处理的在实际运行中不会产生逻辑分支性能不会因分支而下降。另外下面可以看出宏也可以做多层的判断先判断NETEASE_SKINNING宏在内部执行逻辑中又有LARGE_VERTEX_SHADER_UNIFORMS宏的判断

#ifdef NETEASE_SKINNING
		MAT4 boneMat = transpose(mat3x4ToMat4(BONES_70[int(BONEID_0)]));
		entitySpacePosition = boneMat * POSITION;
		entitySpaceNormal = boneMat * NORMAL;
#else
	#if defined(LARGE_VERTEX_SHADER_UNIFORMS)
		entitySpacePosition = BONES[int(BONEID_0)] * POSITION;
		entitySpaceNormal = BONES[int(BONEID_0)] * NORMAL;
	#else
		entitySpacePosition = BONE * POSITION;
		entitySpaceNormal = BONE * NORMAL;
	#endif
#endif

运行时状态

depth 深度测试

depthFunc

深度检测通过函数,可以使用以下的值:

Always : 总是通过
Equal  深度值与缓冲区值相等时通过
NotEqual :深度值与缓冲区值不相等时通过
Less :深度值小于缓冲区值时通过
Greater :深度值大于缓冲区值时通过
GreaterEqual :深度值大于等于缓冲区值时通过
LessEqual :深度值小于等于缓冲区值时通过

相关联的states渲染环境配置

DisableDepthTest  关闭深度测试
DisableDepthWrite  关闭深度写入

Stencil 蒙版测试

stencilRef

与蒙版缓冲区比较或要被写入的值

stencilRefOverride

是否使用缓冲区当前的值作为stencilRef支持0或1

1  使用配置的stencilRef若配置了stencilRef则stencilRefOverride自动取1
0  使用缓冲区当前的值作为stencilRef此情况下不配置stencilRef
stencilReadMask

蒙版缓冲区的值与stencilRef值在比较前均会先与stencilReadMask进行位与运算

stencilWriteMask

stencilRef值在写入蒙版缓冲区前会与stencilWriteMask进行位与运算

frontFace, backFace

配置网格正面或反面使用什么蒙版测试函数,另外,判断的顺序为先蒙版检测,再深度检测,需要配置以下操作:

	stencilFunc : stencilRef与蒙版缓冲区比较时使用的方法支持下面的值
		Always : 总是通过
		Equal  stencilRef与缓冲区值相等时通过
		NotEqual stencilRef与缓冲区值不相等时通过
		Less stencilRef小于缓冲区值时通过
		Greater stencilRef大于缓冲区值时通过
		GreaterEqual stencilRef大于等于缓冲区值时通过
		LessEqual stencilRef小于等于缓冲区值时通过	

	stencilFailOp stencilFunc比较函数返回失败的时候执行的处理支持下面的值
		Keep  保留缓冲区原本数值
		Replace  往缓冲区写入 stencilRef位与stencilWriteMask 的值

	stencilDepthFailOp : stencilFunc比较函数返回成功, 但深度测试失败的时候执行的处理,支持下面的值:
		Keep  保留缓冲区原本数值
		Replace  往缓冲区写入 stencilRef位与stencilWriteMask 的值

	stencilPassOp : stencilFunc比较函数返回成功而且深度测试成功的时候执行的处理支持下面的值
		Keep  保留缓冲区原本数值
		Replace  往缓冲区写入 stencilRef位与stencilWriteMask 的值

相关联的states渲染环境配置

StencilWrite :开启蒙版写入
EnableStencilTest  开启蒙版测试

最后我们看一段例子:

    "shadow_back": {
      "+states": [
        "StencilWrite",
        "DisableColorWrite",
        "DisableDepthWrite",
        "InvertCulling",
        "EnableStencilTest"
      ],

      "vertexShader": "shaders/position.vertex",
      "vrGeometryShader": "shaders/position.geometry",
      "fragmentShader": "shaders/flat_white.fragment",

      "frontFace": {
        "stencilFunc": "Always",
        "stencilFailOp": "Keep",
        "stencilDepthFailOp": "Keep",
        "stencilPassOp": "Replace"
      },

      "backFace": {
        "stencilFunc": "Always",
        "stencilFailOp": "Keep",
        "stencilDepthFailOp": "Keep",
        "stencilPassOp": "Replace"
      },

      "stencilRef": 1,
      "stencilReadMask": 255,
      "stencilWriteMask": 1,

      "vertexFields": [
        { "field": "Position" }
      ],
      "msaaSupport": "Both"
    }

例子中 StencilWrite 代表支持蒙版缓冲区的写入EnableStencilTest代表开启蒙版测试frontFace的配置代表正面渲染的时候蒙版测试总是通过深度测试不通过则保持缓冲区值不变若深度测试也通过则会往缓冲区写入 stencil位与stencilWriteMask 的值,即 1 & 1 = 1值。backFace的配置也是类似。

Blend 半透明对象颜色混合

半透明对象的渲染需要配置混合因子最终输出的rgb颜色值 = 当前颜色值 * 源混合因子 + 缓冲区中的颜色值 * 目标混合因子

blendSrc

源混合因子

blendDst

目标混合因子

alphaSrc

计算alpha时的源混合因子通常不配置取默认值

alphaDst

计算alpha时的目标混合因子通常不配置取默认值


混合因子总共可以取下面的值:

DestColor  缓冲区颜色值
SourceColor  当前颜色值
Zero  (0,0,0)
One  (1,1,1)
OneMinusDestColor : (1,1,1) - 缓冲区颜色值
OneMinusSrcColor : (1,1,1) - 当前颜色值
SourceAlpha  当前颜色中的alpha值
DestAlpha  缓冲区颜色中的alpha值
OneMinusSrcAlpha  1 - 当前颜色值中的alpha值

在引擎中,默认值为:

blendSrc SourceAlpha 
blendDst OneMinusSrcAlpha
alphaSrc One
alphaDst OneMinusSrcAlpha

相关联的states渲染环境配置

Blending : 开启颜色混合模式常用于渲染半透明对象。声明这个之后通常也需要声明混合因子blendSrc,blendDst
DisableColorWrite  不往颜色缓冲区写入颜色值RGBA通道均不写入
DisableAlphaWrite  不往颜色缓冲区写入透明度alpha值允许写入RGB值
DisableRGBWrite  不往颜色缓冲区写入透明度RGB值允许写入Alpha值

sample 纹理采样

samplerStates

配置采样状态值为一个列表根据需要采样的纹理个数为每一个纹理进行配置通常若顶点属性中声明了UV0, UV1代表需要采样两个纹理这里则需要配置两个元素。下面看子元素的定义

{
	"samplerIndex": 0,
	"textureFilter": "Point",
	"textureWrap": "Repeat"
}

每个属性的定义如下:

samplerIndex

数字代表当前正在设置第几张纹理的属性由0开始

textureFilter

纹理过滤模式默认为Point当实际显示的纹理贴图相比于原图进行了放大或缩小时新的分辨率贴图与原分辨率贴图上像素点的映射关系可以有以下的值

	Point  点采样
	Bilinear : 双线性采样
	Trilinear : 三线性采样
	MipMapBilinear  MipMap双线性采样
	TexelAA :纹素抗锯齿(不是所有设备都支持,不建议使用)
	PCF :通过比较函数进行采样(不是所有设备都支持,不建议使用)
textureWrap

纹理包裹模式控制uv若在[0,1]之外的时候应该采样到什么样的纹理,可以有如下的值:

	Repeat  重复,即把值求模到[01]之间进行采样
	Clamp  边缘采样,采样最靠近的边缘的值,即1.1比较靠近1,则取1-0.1比较靠近0,则取0

vertex 顶点属性

vertexFields

顶点属性,用于声明使用这个材质进行渲染的网格每个顶点保存有什么属性,由美术制作资源的时候决定,可能用到的有以下的值:

Position  模型空间坐标
Color  颜色
Normal  法线
UV0 :纹理采样坐标
UV1 :纹理采样坐标
UV2 :纹理采样坐标
BoneId0  骨骼ID骨骼模型中用到

rasterizer 光栅化环境配置

msaaSupport

配置MSAA(多重采样抗锯齿)的支持(引擎中的默认值为NonMSAA)

NonMSAA : 在没有开启MSAA的时候材质允许使用
MSAA : 在开启MSAA的时候材质允许使用
Both 无论是否开启MSAA材质都允许使用。通常使用这个值就可以了。
深度偏移

深度偏移主要用于解决z-fighting问题即当两个物体深度相近则渲染时可能会出现某些帧显示这个物体某些帧显示另一个物体这种闪烁现象。深度偏移的原理是把其中一个对象往深度大或者小的方向偏移一下使他们的深度不再一样。 可配置以下四个变量:

depthBias
slopeScaledDepthBias
depthBiasOGL
slopeScaledDepthBiasOGL

具体偏移的深度为:

offset = (slopeScaledDepthBias * m) + (depthBias * r)

在OGL平台上则为

offset = (slopeScaledDepthBiasOGL * m) + (depthBiasOGL * r)

m是多边形的深度的斜率在光栅化阶段计算得出中的最大值。一个多边形越是与近裁剪面平行m就越接近0。 r是能产生在窗口坐标系的深度值中可分辨的差异的最小值r是由具体实现OpenGL的平台指定的一个常量。


相关联的states渲染环境配置

Wireframe  绘制线框模式
DisableCulling : 同时渲染正面和反面
InvertCulling :使用正面裁剪。默认情况下为背面裁剪,声明这个之后则渲染背面,裁剪掉正面

primitive 图元

primitiveMode

图元渲染模式引擎中的默认值为TriangleList

None  不渲染,正常情况下不会为这个值
QuadList :四边形模式
TriangleList : 每三个顶点绘制一个三角形的模式例如第一个三角形使用顶点v0,v1,v2,第二个使用v3,v4,v5
TriangleStrip : 每一个顶点会与前两个出现的顶点构成三角形,结构复杂一点,但会节省数据量
LineList : 每两个顶点绘制一条线段
Line : 每一个顶点会与前一个出现的一个顶点构成线段。

材质变体

variants

用于快速基于大部分相同定义实现多种子材质。看下面entity_static这个实际例子

    "entity_static": {
      "vertexShader": "shaders/entity.vertex",
      "vrGeometryShader": "shaders/entity.geometry",
      "fragmentShader": "shaders/entity.fragment",
      "vertexFields": [
        { "field": "Position" },
        { "field": "Normal" },
        { "field": "UV0" }
      ],
      "variants": [
        {
          "skinning": {
            "+defines": [ "USE_SKINNING" ],
            "vertexFields": [
              { "field": "Position" },
              { "field": "BoneId0" },
              { "field": "Normal" },
              { "field": "UV0" }
            ]
          }
        },
        {
          "skinning_color": {
            "+defines": [ "USE_SKINNING", "USE_OVERLAY" ],
            "+states": [ "Blending" ],
            "vertexFields": [
              { "field": "Position" },
              { "field": "BoneId0" },
              { "field": "Color" },
              { "field": "Normal" },
              { "field": "UV0" }
            ]
          }
        }
      ],
      "msaaSupport": "Both",
      "+samplerStates": [
        {
          "samplerIndex": 0,
          "textureFilter": "Point"
        }
      ]
    },

variants即为材质变体的声明上述声明了skinning和skinning_color两个子变体子变体中对外部某些字段进行了改写。实际使用中相当于快速定义了两个材质本体和变体间用点"."连接两个材质分别为entity_static.skinning和entity_static.skinning_color。

除此之外后续如果有其它材质继承自entity_static比如entity_dynamic则此材质也会同时继承有此两种变体分别为entity_dynamic.skinning和entity_dynamic.skinning_color。

材质合并规则

不同目录文件中声明了同一材质时的,在加载后会根据以下规则进行合并: 1.通常情况下后加载的文件的材质的字段会覆盖之前加载的 2.以下字段特殊,除了替换以外,还支持使用"+"添加与使用"-"删除属性的操作:

defines
states
samplerStates

举一个的例子,例如包体文件中声明了这么一材质(省略无关代码),定义了三个宏:

"testMat": {
	"defines": [ "MACRO_1", "MACRO_2", "MACRO_3" ],
}

此时一个Mod也声明了此材质定义了另外三个宏

"testMat": {
	"defines": [ "MACRO_4", "MACRO_5", "MACRO_6" ],
}

上述情况下最终运行时相当于defines字段被覆盖实际运行时生效的宏只有: MACRO_4, MACRO_5, MACRO_6

若MOD中定义的时候使用了"+"符号:

"testMat": {
	"+defines": [ "MACRO_4", "MACRO_5", "MACRO_6" ],
}

相当于在原来的基础上添加定义,则实际运行时生效的宏有: MACRO_1, MACRO_2, MACRO_3, MACRO_4, MACRO_5, MACRO_6

若MOD中定义的时候使用了"-"符号:

"testMat": {
	"-defines": [ "MACRO_3"],
}

相当于在原来的基础上删除某些定义,则实际运行时生效的宏只有: MACRO_1, MACRO_2

若多个文件都对同一材质进行了定义,而且分别涉及有覆盖,添加,删除操作,则依次生效的顺序为: 先执行所有的覆盖操作,再执行所有的添加操作,最后执行所有的删除操作。

即如果有其中一个材质文件声明删除MACRO_3操作

"testMat": {
	"-defines": [ "MACRO_3"],
}

则无论其它文件怎么覆盖添加MACRO_3最终合成后这一个材质一定不会有MACRO_3宏。