材质配置文件说明
WARNING
材质配置不适合心理承受能力弱者。请做好可能遇到游戏崩溃、内容日志报错以及长时间加载的准备。
前言
本文译自网易中国版开发者手册(https://mc.163.com/dev/mcmanual/mc-dev/mcguide/),将详细解析材质文件的结构与配置方式。
材质文件体系
我们将以微软原生材质文件为例进行说明。首先,目录下的文件主要是以".material"为后缀的文件。此外还有三个重要的json文件:common.json、fancy.json和sad.json。
先来看sad.json和fancy.json,它们用于控制画面质量表现。各自定义了一个材质文件列表。fancy.json通常会比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"}
][
{"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.json中的材质文件通常会有更复杂的运算,而sad.json中的材质通常会牺牲一点表现效果换取更好的性能。如果开发者需要编写更复杂的着色器,建议同时编写一个低配版本,然后分别在fancy和sad中定义。让玩家通过游戏中的精美图像选项来控制是否开启对应效果。
[
{"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,这些都是固定格式,标识这个材质文件的解析方式,我们可以暂时忽略不做修改。
可以看到材质中每个字段的定义都是以键值对的形式,例如:
[
"vertexShader": "shaders/entity.vertex",
]冒号左边代表键vertexShader,右边代表值shaders/entity.vertex;
也有列表形式的定义:
[
"vertexFields": [
{ "field": "Position" },
{ "field": "Normal" },
{ "field": "UV0" }
],
]带有符号[ ]的就是一个列表,然后里面是每个子元素的json定义。
材质所有属性字段概览
渲染状态
states
配置渲染环境,可以有以下取值:
EnableAlphaToCoverage:一种针对半透明物体的顺序无关渲染方式。此开关仅在支持MSAA的环境中有用。开启后物体边缘会根据透明度更精确地柔化过渡。也可用于一些网格大量重叠的复杂场景。Wireframe:线框绘制模式Blending:开启颜色混合模式,常用于渲染半透明物体。声明此项后通常需要声明混合因子blendSrc、blendDstDisableColorWrite:不向颜色缓冲写入颜色值,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。
着色器宏定义
defines
定义所用着色器的宏。为了实现代码复用,我们很多不同的材质使用的是同一个着色器。此时如果想在着色器某处根据当前材质执行不同逻辑,可以通过材质defines声明的宏来判断。我们可以用材质entity_for_skeleton举例说明。这里可以看到定义了USE_SKINNING、USE_OVERLAY、NETEASE_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来判断宏并执行不同逻辑分支。这些宏的判断语句是在编译时处理的,不同于传统着色器中的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运行时状态
深度测试
depthFunc
深度检测通过函数,可以使用以下取值:
Always:总是通过Equal:当深度值等于缓冲值时通过NotEqual:当深度值不等于缓冲值时通过Less:当深度值小于缓冲值时通过Greater:当深度值大于缓冲值时通过GreaterEqual:当深度值大于等于缓冲值时通过LessEqual:当深度值小于等于缓冲值时通过
关联states渲染环境配置:
DisableDepthTest:关闭深度测试DisableDepthWrite:关闭深度写入
模板掩码测试
stencilRef
用于与掩码缓冲比较或写入的值
stencilRefOverride
是否使用缓冲当前值作为stencilRef,支持0或1:
1:使用配置的stencilRef。如果配置了stencilRef,stencilRefOverride会自动取10:使用缓冲当前值作为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的配置也类似。
混合半透明物体颜色混合
半透明物体的渲染需要配置混合因子。最终输出的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:SourceAlphablendDst:OneMinusSrcAlphaalphaSrc:OnealphaDst:OneMinusSrcAlpha
关联states渲染环境配置:
Blending:开启颜色混合模式,常用于渲染半透明物体。声明此项后通常需要声明混合因子blendSrc、blendDstDisableColorWrite:不向颜色缓冲写入颜色值,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:Texel抗锯齿(非所有设备支持,不建议使用)PCF:通过比较函数采样(非所有设备支持,不建议使用)
textureWrap
纹理包裹模式,控制当uv在[0,1]之外时应该采样什么样的纹理。可以有如下取值:
Repeat:重复,即对值取模到[0,1]进行采样Clamp:边缘采样,采样最近边缘的值,即如果1.1离1更近则取1;-0.1离0更近则取0。
顶点
vertexFields
顶点属性,用于声明使用这个材质渲染的网格的每个顶点持有哪些属性。由美术在生产资源时决定。可能会用到以下取值:
Position:模型空间坐标Color:颜色Normal:法线UV0:纹理采样坐标UV1:纹理采样坐标UV2:纹理采样坐标BoneId0:骨骼ID,用于骨骼模型
光栅化环境配置
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:使用正面裁剪。默认为背面裁剪。声明此项后渲染背面,正面被裁剪。
图元
primitiveMode
图元渲染模式(引擎中默认为TriangleList):
None:不渲染,正常不会使用QuadList:四边形模式TriangleList:每三个顶点绘制一个三角形的模式,例如第一个三角形使用顶点v0、v1、v2,第二个使用v3、v4、v5TriangleStrip:每个顶点会与前两个顶点形成一个三角形,结构稍复杂,

