Files
netease-bedrock-wiki/mcguide/16-美术/7-材质与着色器/3-材质配置说明.md
2025-08-25 18:36:29 +08:00

633 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
front:
hard: 入门
time: 20分钟
---
# 材质配置说明
## 前言
本文将详细介绍材质文件的结构与配置方式。
## 材质文件的加载
材质文件存放于资源包的materials文件夹下我们打开resource_packs\vanilla\materials目录可看到包含有下面这些文件这些是微软原生定义的材质文件
![material_list](./images/material_list.png)
而resource_packs\vanilla_netease\materials目录下的材质文件则是网易对原生材质文件的修改与扩充。
下面我们就先以原生微软的材质文件进行讲解,首先,目录下面的文件基本都是以".material"为后缀的文件除此之外还有3个重要的json文件分别是common.jsonfancy.jsonsad.json。
我们先来看看sad.json和fancy.json他们是用于控制画质表现的内部各自定义了一个材质文件列表fancy.json通常比sad.json多定义几个材质文件并且会为某些材质文件多添加一些额外的宏这些宏会使shader采用不同的代码块实现对画质和性能的控制
```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"}
]
```
```json
[ // 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和hologram两个材质文件并且为某些材质文件内的所有材质增加了`FANCY`宏。开发者可以在shader中通过`FANCY`宏来选择不同的计算策略:
```glsl
#ifdef FANCY
// 完整版效果
#else
// 青春版效果
#endif
```
通过游戏内的`设置/视频/精美图像`开关可以实现fancy材质和sad材质的切换。当`精美图像`开关打开时fancy.json中的材质文件就会生效反之sad.json中的材质文件就会生效
![fancy_switch](./images/fancy_switch.png)
为了实现更好的表现效果fancy.json中的材质通常使用较复杂的运算而sad.json中的材质则通过牺牲渲染表现来换取更好的性能。
开发者若需要编写开销较大的材质shader可以考虑利用`FANCY`宏同时编写一个低消耗的sad版本然后分别把材质分别放在fancy与sad内定义的相应的材质文件中玩家可以在游戏中通过`精美图像`开关自行控制是否开启相应效果如果开发者不需要区分材质的性能开销则可以考虑仅使用common.json所有在common.json中定义的材质文件都会被加载。
## 材质定义与引用
在此我们将简单介绍MC材质是如何被定义与引用的。
### 材质定义
假设有一个开发者自定义的材质文件`example.material`,它的基本格式框架如下:
```json
{ // example.material
"materials": {
"version": "1.0.0", // 表示material文件格式版本 必不可少
// 材质1
"mat_example": {
// ...具体的材质定义
},
// 材质2 它引用了mat_example
"other_mat:mat_example": {
// ...具体的材质定义
}
}
}
```
可以看到,所有的材质定义都被包含在`materials`这个字段内,并且有一个`version`字段也被包含在`materials`内,用来标识这个材质文件的格式版本。我们还可以注意到这样的字符串:`"other_mat:mat_example"`,它表示材质`other_mat`继承自材质`mat_example`,继承后的材质具有父材质的所有属性,在此基础上,子材质`other_mat`还可以新增属性,或者覆盖父材质中已有的属性。
接下来看材质`mat_example`的具体配置格式:
```json
"mat_example": {
"vertexShader": "shaders/glsl/mat_example.vertex",
"fragmentShader": "shaders/glsl/mat_example.fragment",
"+defines": [
"USE_SKINNING",
"USE_OVERLAY",
"NETEASE_SKINNING"
],
"+samplerStates": [
{
"samplerIndex": 0,
"textureFilter": "Point"
}
],
"states": [
"Blending",
"DisableDepthWrite"
]
}
```
以下是对材质`mat_example`配置的解读:
* `vertexShader``fragmentShader`定义了这个材质使用的顶点着色器和片元着色器的路径,开发者需要自行实现对应的着色器
* `+defines`定义了材质启用的宏定义在shader中这些宏定义将会生效
* `+samplerStates`定义了shader接受的采样纹理列表纹理个数仅为1其中`samplerIndex`为0表示材质启用的纹理是`TEXTURE_0``textureFilter`指定了纹理采样的过滤方式为点过滤/点滤波
* `state`定义了材质额外开启的渲染状态,其中的`Blending`表示使用alpha混合`DisableDepthWrite`表示材质将不会影响屏幕的深度信息。
更具体的材质配置参数可以参考本文档的其余部分。
### 材质引用
接上节,我们定义了一个名为`mat_example`的自定义材质,根据材质类型不同,引用材质的方式也有所不同。
#### 网易骨骼模型引用材质
网易骨骼模型配置写在`vanilla_netease/models/netease_models.json`文件中,开发者可以看到类似这样的模型配置段:
```json
"model_name": {
"dy_load": true,
"mesh": "mesh/model_name_mesh.json",
"skeleton": "skeleton/model_name_skeleton.json"
}
```
这段配置没有给出引用的材质名,因此游戏会在渲染这个模型时默认使用`entity_for_skeleton`材质,我们可以增加一个`material`字段,显式地引用我们在`entity.material`文件中自定义的材质:
```json
"model_name": {
"dy_load": true,
"mesh": "mesh/model_name_mesh.json",
"skeleton": "skeleton/model_name_skeleton.json",
"material": "mat_example"
}
```
除此之外我们还支持开发者使用多pass特性来加强骨骼模型的渲染效果。假设开发者希望让一个模型在单帧内使用不同材质各渲染一次则可以将`material`字段值改为数组:
```json
"material": ["mat0", "mat1", "mat2"]
```
该模型将在每一帧**按顺序**渲染数组中所引用的材质。
在游戏内调取出这个模型,就可以预览到自定义材质的效果。
> 骨骼模型所引用的材质,其着色器需要支持`NETEASE_SKINNING`宏的功能,具体可以参考`entity_for_skeleton`材质的着色器实现。
#### 原版模型引用材质
在实体配置文件`vanilla/entity/xxx.entity.json`中,我们可以找到与材质有关的配置内容:
```json
{
"format_version": "1.8.0",
"minecraft:client_entity": {
"description": {
"identifier": "netease:xxx",
"materials": {
"default": "xxxxx",
... // 其他材质配置
},
... // 其他字段
}
}
}
```
其中`materials`中的`default`字段,就指定了这个实体在常态下使用的材质,我们可以将这个字段的值修改为我们自定义的材质,让这个实体在渲染时具有我们自定义的效果:
```json
"default": "mat_example"
```
> 实体模型材质使用的着色器,需要支持`USE_SKINNING`宏,具体可以参考`entity_static_netease`或者`entity_static`材质的实现。
#### 后处理引用材质
开发者可以在`vanilla_netease/graphics_settings/post_process.json`找到后处理相关配置,在配置文件的`process_array`字段下,我们可以看到若干个后处理的定义,在此以老电视机效果作为例子:
```json
{
"name": "oldtv",
"enable": false,
"paras": [
{ "name": "density", "value": 0.1, "range": [0.0, 1.0] },
{ "name": "strength", "value": 1.0, "range": [0.0, 1.0] },
{ "name": "snow_size", "value": 2.0, "range": [0.5, 16.0] },
{ "name": "noise_fps", "value": 6.0, "range": [0.01, 64.0] },
{ "name": "black_zone", "value": 0.2, "range": [0.0, 1.0] }
],
"pass_array":[ // 后处理pass数组
{ // 第0个pass
"render_target":{
"width":1.0,
"height":1.0
},
"material":"old_tv" // 第0个pass使用的材质
}
]
}
```
该配置定义了后处理的名称、默认是否开启、着色器参数、后处理pass数组等信息。可以看到后处理pass数组中的单个pass包含一个`material`字段这代表了这个pass将使用的材质我们可以修改它的值改变这个pass使用的材质。
> 制作后处理材质需要定义某些特殊的材质配置,并且着色器要基于全屏范围进行计算,具体可以参考`vanilla_netease/materials/postprocess.material`中的`oldtv`材质
#### 中国版粒子特效引用材质
中国版粒子特效可以使用网易MCStudio生成制作。中国版粒子特效具有默认材质因此在特效配置中并没有显式地记录引用材质的名称。开发者可以在中国版粒子特效中加入如下字段来改变粒子特效所引用的材质
```json
"materialname": {
"value": "my_material"
}
```
## 材质属性
我们把所有字段按功能分到以下几类中:
### 渲染状态
#### states
配置渲染环境,可以有以下的值:
```json
EnableAlphaToCoverage MSAA
Wireframe 线
Blending : blendSrc,blendDst
DisableColorWrite RGBA
DisableAlphaWrite alphaRGB
DisableRGBWrite RGBAlpha
DisableDepthTest
DisableDepthWrite
DisableCulling : 使
InvertCulling 使
StencilWrite
EnableStencilTest
```
### 着色器路径
#### vertexShader
顶点着色器的路径通常为shaders/glsl/XXX.vertex。
#### vrGeometryShader 或 geometryShader
几何着色器的路径通常为shaders/glsl/XXX.geometry移动端用不到无须考虑修改。
#### fragmentShader
片段着色器的路径通常为shaders/glsl/XXX.fragment。
### Shader宏定义
#### defines
为使用到的Shader定义宏。为了代码的复用我们很多不同的材质会使用相同的shader。此时若希望shader里面某处根据当前材质执行不一样的逻辑则可以通过材质defines声明的宏去做判断。
我们可以用`entity_for_skeleton`这个材质为例它定义了USE_SKINNINGUSE_OVERLAYNETEASE_SKINNING三个宏。
```json
"+defines": [ "USE_SKINNING", "USE_OVERLAY", "NETEASE_SKINNING" ]
```
于是在着色器可以中通过`#ifdef``#if`等语句来对宏进行判断并执行不同的逻辑,宏的判断语句是编译期处理的,性能不会因分支而下降:
```json
#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 // defined(LARGE_VERTEX_SHADER_UNIFORMS)
#endif // NETEASE_SKINNING
```
### 运行时状态
#### depth 深度测试
##### depthFunc
深度检测通过函数,可以使用以下的值:
```json
Always :
Equal
NotEqual
Less
Greater
GreaterEqual
LessEqual
```
相关联的states渲染环境配置
```json
DisableDepthTest
DisableDepthWrite
```
#### Stencil 蒙版测试
##### stencilRef
与蒙版缓冲区比较或要被写入的值
##### stencilRefOverride
是否使用缓冲区当前的值作为stencilRef支持0或1
```json
1 使stencilRefstencilRefstencilRefOverride1
0 使stencilRefstencilRef
```
##### stencilReadMask
蒙版缓冲区的值与stencilRef值在比较前均会先与stencilReadMask进行位与运算
##### stencilWriteMask
stencilRef值在写入蒙版缓冲区前会与stencilWriteMask进行位与运算
##### frontFace, backFace
配置网格正面或反面使用什么蒙版测试函数,另外,判断的顺序为先蒙版检测,再深度检测,需要配置以下操作:
```json
stencilFunc : stencilRef使
Always :
Equal stencilRef
NotEqual stencilRef
Less stencilRef
Greater stencilRef
GreaterEqual stencilRef
LessEqual stencilRef
stencilFailOp stencilFunc
Keep
Replace stencilRefstencilWriteMask
stencilDepthFailOp : stencilFunc,
Keep
Replace stencilRefstencilWriteMask
stencilPassOp : stencilFunc
Keep
Replace stencilRefstencilWriteMask
```
相关联的states渲染环境配置
```json
StencilWrite
EnableStencilTest
```
最后我们看一段例子:
```json
"shadow_back": {
"+states": [
"StencilWrite",
"DisableColorWrite",
"DisableDepthWrite",
"InvertCulling",
"EnableStencilTest"
],
"frontFace": {
"stencilFunc": "Always",
"stencilFailOp": "Keep",
"stencilDepthFailOp": "Keep",
"stencilPassOp": "Replace"
},
"backFace": {
"stencilFunc": "Always",
"stencilFailOp": "Keep",
"stencilDepthFailOp": "Keep",
"stencilPassOp": "Replace"
},
"stencilRef": 1,
"stencilReadMask": 255,
"stencilWriteMask": 1,
// 省略其余部分
}
```
例子中`StencilWrite`代表支持蒙版缓冲区的写入,`EnableStencilTest`代表开启蒙版测试,`frontFace`配置了正向片元的模板测试策略,代表正向片元的蒙版测试总是通过(Always),深度测试不通过则保持缓冲区值不变(Keep),若深度测试通过则会将模板缓冲更新为`stencilRef`的值。`backFace`同理。
#### Blend 半透明对象颜色混合
半透明对象的渲染需要配置混合因子最终输出的rgb颜色值 = 当前颜色值 * 源混合因子 + 缓冲区中的颜色值 * 目标混合因子
##### blendSrc
源混合因子
##### blendDst
目标混合因子
##### alphaSrc
计算alpha时的源混合因子通常不配置取默认值
##### alphaDst
计算alpha时的目标混合因子通常不配置取默认值
---
混合因子总共可以取下面的值:
```json
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
```
在引擎中,默认值为:
```json
blendSrc SourceAlpha
blendDst OneMinusSrcAlpha
alphaSrc One
alphaDst OneMinusSrcAlpha
```
相关联的states渲染环境配置
```json
Blending : blendSrc,blendDst
DisableColorWrite RGBA
DisableAlphaWrite alphaRGB
DisableRGBWrite RGBAlpha
```
#### sample 纹理采样
##### samplerStates
配置采样状态值为一个列表根据需要采样的纹理个数为每一个纹理进行配置通常若顶点属性中声明了UV0, UV1代表需要采样两个纹理这里则需要配置两个元素。下面看子元素的定义
```json
{
"samplerIndex": 0,
"textureFilter": "Point",
"textureWrap": "Repeat"
}
```
每个属性的定义如下:
samplerIndex
数字代表当前正在设置第几张纹理的属性由0开始
textureFilter
纹理过滤模式默认为Point当实际显示的纹理贴图相比于原图进行了放大或缩小时新的分辨率贴图与原分辨率贴图上像素点的映射关系可以有以下的值
```json
Point
Bilinear : 线
Trilinear : 线
MipMapBilinear MipMap线
TexelAA 齿使
PCF 使
```
textureWrap
纹理包裹模式控制uv若在[0,1]之外的时候应该采样到什么样的纹理,可以有如下的值:
```json
Repeat [01]
Clamp 1.111-0.100
```
#### vertex 顶点属性
##### vertexFields
顶点属性,用于声明使用这个材质进行渲染的网格每个顶点保存有什么属性,由美术制作资源的时候决定,可能用到的有以下的值:
```json
Position
Color
Normal 线
UV0
UV1
UV2
BoneId0 ID
```
#### rasterizer 光栅化环境配置
##### msaaSupport
配置MSAA(多重采样抗锯齿)的支持(引擎中的默认值为NonMSAA)
```json
NonMSAA : MSAA使
MSAA : MSAA使
Both MSAA使使
```
##### 深度偏移
深度偏移主要用于解决z-fighting问题即当两个物体深度相近则渲染时可能会出现某些帧显示这个物体某些帧显示另一个物体这种闪烁现象。深度偏移的原理是把其中一个对象往深度大或者小的方向偏移一下使他们的深度不再一样。
可配置以下四个变量:
```json
depthBias
slopeScaledDepthBias
depthBiasOGL
slopeScaledDepthBiasOGL
```
具体偏移的深度为:
```json
offset = (slopeScaledDepthBias * m) + (depthBias * r)
```
在OGL平台上则为
```json
offset = (slopeScaledDepthBiasOGL * m) + (depthBiasOGL * r)
```
m是多边形的深度的斜率在光栅化阶段计算得出中的最大值。一个多边形越是与近裁剪面平行m就越接近0。
r是能产生在窗口坐标系的深度值中可分辨的差异的最小值r是由具体实现OpenGL的平台指定的一个常量。
---
相关联的states渲染环境配置
```json
Wireframe 线
DisableCulling :
InvertCulling 使
```
#### primitive 图元
##### primitiveMode
图元渲染模式引擎中的默认值为TriangleList
```json
None
QuadList
TriangleList : 使v0,v1,v2,使v3,v4,v5
TriangleStrip :
LineList : 线
Line : 线
```
### 材质变体
#### variants
用于快速基于大部分相同定义实现多种子材质。看下面entity_static这个实际例子
```json
"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.以下字段特殊,除了替换以外,还支持使用"+"添加与使用"-"删除属性的操作:
```json
defines
states
samplerStates
```
举一个的例子,例如包体文件中声明了这么一材质(省略无关代码),定义了三个宏:
```json
"testMat": {
"defines": [ "MACRO_1", "MACRO_2", "MACRO_3" ],
}
```
此时一个Mod也声明了此材质定义了另外三个宏
```json
"testMat": {
"defines": [ "MACRO_4", "MACRO_5", "MACRO_6" ],
}
```
上述情况下最终运行时相当于defines字段被覆盖实际运行时生效的宏只有: MACRO_4, MACRO_5, MACRO_6
若MOD中定义的时候使用了"+"符号:
```json
"testMat": {
"+defines": [ "MACRO_4", "MACRO_5", "MACRO_6" ],
}
```
相当于在原来的基础上添加定义,则实际运行时生效的宏有: MACRO_1, MACRO_2, MACRO_3, MACRO_4, MACRO_5, MACRO_6
若MOD中定义的时候使用了"-"符号:
```json
"testMat": {
"-defines": [ "MACRO_3"],
}
```
相当于在原来的基础上删除某些定义,则实际运行时生效的宏只有: MACRO_1, MACRO_2
若多个文件都对同一材质进行了定义,而且分别涉及有覆盖,添加,删除操作,则依次生效的顺序为:
先执行所有的覆盖操作,再执行所有的添加操作,最后执行所有的删除操作。
即如果有其中一个材质文件声明删除MACRO_3操作
```json
"testMat": {
"-defines": [ "MACRO_3"],
}
```
则无论其它文件怎么覆盖添加MACRO_3最终合成后这一个材质一定不会有MACRO_3宏。