Files
netease-modsdk-wiki/docs/mcguide/16-美术/7-材质与着色器/3-材质配置说明.md
2025-03-18 14:46:12 +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 不往颜色缓冲区写入透明度alpha值允许写入RGB值
DisableRGBWrite 不往颜色缓冲区写入透明度RGB值允许写入Alpha值
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 使用配置的stencilRef若配置了stencilRef则stencilRefOverride自动取1
0 使用缓冲区当前的值作为stencilRef此情况下不配置stencilRef
```
##### 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 往缓冲区写入 stencilRef位与stencilWriteMask 的值
stencilDepthFailOp : stencilFunc比较函数返回成功, 但深度测试失败的时候执行的处理,支持下面的值:
Keep 保留缓冲区原本数值
Replace 往缓冲区写入 stencilRef位与stencilWriteMask 的值
stencilPassOp : stencilFunc比较函数返回成功而且深度测试成功的时候执行的处理支持下面的值
Keep 保留缓冲区原本数值
Replace 往缓冲区写入 stencilRef位与stencilWriteMask 的值
```
相关联的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 不往颜色缓冲区写入透明度alpha值允许写入RGB值
DisableRGBWrite 不往颜色缓冲区写入透明度RGB值允许写入Alpha值
```
#### 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.1比较靠近1,则取1-0.1比较靠近0,则取0
```
#### 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宏。