Cesium中的图形技术:Fabric —— 材质JSON规范
1. 简介
Fabric 是 Cesium 中用于描述材质的一种 JSON 规定。
材质表现了多边形、折线、椭圆等形状的外观。
使用 Fabric 和 GLSL,可以完全自定义材质。
通过几何对象的 material
属性可以创建材质,这个属性是 Cesium.Material
对象。
可以这么用:
// 假设 polygon 是一个 primitive
polygon.appearance.material = Cesium.Material.fromType('color');
这就创建了一个只有颜色的材质,包括透明度的颜色。Cesium.Material.fromType()
方法是一个简写,完整的写法是:
polygon.appearance.material = new Cesium.Material({
fabric: {
type: 'Color' // 大写
}
})
每一个 Material 都可以有 0 ~ N 个 uniform,这个参数在创建时指定,也可以在渲染后修改。例如,color 类型的 Material 就有格式为 rgba 的颜色 uniform:
polygon.appearance.material = new Cesium.Material({
fabric: {
type: 'Color',
uniforms: {
color: new Cesium.Color(1.0, 0.0, 0.0, 0.5)
}
}
})
// 修改颜色
polygon.appearance.material.uniforms.color = Cesium.Color.WHITE
2. 内置材质(共计23种,ver1.75)
Cesium 有几个内置的材质。列举两个比较常用的:
材质类型 | 截图 | 描述 |
---|---|---|
type: 'Color' |
一个简单的颜色,包括透明通道 | |
type: 'Image' |
jpg 或 png 贴图类型的材质 |
所有的内置材质可以简单地使用 Cesium.Material.fromType()
方法创建:
polygon.appearance.material = Cesium.Material.fromType('Image')
polygon.appearance.material.uniforms.image = 'image.png'
或者用全写法:
polygon.appearance.material = new Cesium.Material({
fabric: {
type: 'Image',
uniforms: {
image: 'image.png'
}
}
})
从这儿开始,介绍因这个 fabric 对象中的 type
不同的十几种内置纹理,2.1~2.5
2.1. 机器生成的规律纹理(4种)
只需指定几个参数,就可以生成一些有规律的纹理贴图,不需要依赖外部贴图文件。它们相当于漫反射+透明度的组合。
类型 | 截图 | 描述 |
---|---|---|
type: 'Checkerboard' |
国际象棋格子 | |
type: 'Stripe' |
竖条纹旗帜 | |
type: 'Dot' |
行列点阵 | |
type: 'Grid' |
线状网格,显示一些网状结构的图形 |
2.2. 基础材质(6种)
基础材料表达的是各个材质因子表示的材料特征,例如镜面反射强度、自发光。通常,组合在一个 fabric 对象中创建复杂的材质。
注:如果不懂这些东西,可以请教技术美工。
类型 | 截图 | 描述 |
---|---|---|
type: 'DiffuseMap' |
漫反射贴图,即最常见的贴图,通常是 rgb 三个颜色 | |
type: 'SpecularMap' |
单通道贴图,表示的是入射光强度贴图 | |
type: 'AlphaMap' |
单通道的不透明度贴图 | |
type: 'NormalMap' |
三通道贴图,表示的是法线贴图 | |
type: 'BumpMap' |
单通道的凹凸贴图 | |
type: 'EmissionMap' |
三通道的自发光贴图 |
2.3. 折线材质(3种)
折线材质只作用于折线图形。
类型 | 截图 | 描述 |
---|---|---|
type: 'PolylineArrow' |
箭头线,终点在折线末端 | |
type: 'PolylineGlow' |
发光线 | |
type: 'PolylineOutline' |
描边线 |
2.4. Misc 材质(2种)
还有一些材质不属于上面的分类,例如:
类型 | 截图 | 描述 |
---|---|---|
type: 'Water' |
水面贴图,看起来有水波动效 | |
type: 'RimLighting' |
边缘会比较亮 |
2.5. 公共 uniforms
许多材质是有 image
的,可能是一个 base64 编码的字符串或文件路径:
polygon.appearance.material.uniforms.image = 'image.png';
polygon.appearance.material.uniforms.image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAC/SURBVDhPrZPRDYQgEEQpjVKuFEvhw0IoxU6QgQwMK+vdx5FsooT3GHdjCM4qZnnnHvvkYoxFi/uvIhwiRCClXFC6v5UQ1uQAsbrkHCLsbaPjFgIzQQc1yUOwu33ePGE3BQUaee2BpjhbP5YUmkAlbNzsAURfBDqJnMIyyv4JjsCCgCnIR32uZUfcJuGBOwEk6bOKhoAADh31EIq3MgFg1mgkE1BA2AoUZoo2iZ3gyqGgmMDC/xWwkfb3/eUd7A1v3kxjNW9taQAAAABJRU5ErkJggg==';
有的材质要求贴图有三个颜色分量,而其他材料(如高光和透明贴图)的贴图只需要一个颜色分量。
可以指定贴图的通道。例如,镜面反射的材质中,镜面反射默认取贴图的自 r 颜色通道,但是可以修改它为 a 通道:
polygon.appearance.material = new Cesium.Material({
fabric : {
type : 'SpecularMap',
uniforms : {
image : 'specular.png',
channel : 'a'
}
}
});
这意味着,允许把各种贴图集中到一个贴图文件种,然后使用不同的通道即可,减少加载请求次数。
有些材质的贴图纹理可以重复多次绘制,例如水平或垂直上的重复:
polygon.appearance.material = new Cesium.Material({
fabric: {
type: 'DiffuseMap',
uniforms: {
image: 'diffuse.png',
repeat: {
x: 10,
y: 2
}
}
}
})
3. 创建新材质
使用 fabric 对象 + GLSL 代码和其他素材,就可以创建材质。
如果一个材质不想被复用,那么就不要指定它的 type
属性。
let fabric = {
// ...
}
polygon.appearance.material = new Cesium.Material({
fabric: fabric
})
当 fabric 对象中的 type
属性之前是没有指定过的,那么在第一次调用 new Cesium.Material()
时,这个新的 fabric 材质将被缓存,随后再次 new Material 或 Material.fromType() 时将从缓存中取用。
let fabric = {
type : 'MyNewMaterial',
// ...其他 fabric JSON 的属性
}
polygon.appearance.material = new Cesium.Material({
fabric : fabric
});
// ... 然后在另一处需要这个 fabric
anotherPolygon..appearance.material = Material.fromType('MyNewMaterial');
3.1. 组件
白色的漫反射材质或许是最常用的:
let fabric = {
components: {
diffuse: 'vec3(1.0)'
}
}
稍微复杂一些,加一点镜面反射,使得正射视角看反光最强,侧面变弱:
let fabric = {
components : {
diffuse : 'vec3(0.5)',
specular : '0.1'
}
}
components
属性包含了 fabric 所定义的材质的各种子因素。每个子因素均使用简短的 glsl 代码字符串表示,因此上面写的 vec(0.5)
就表示 rgb 均为 0.5 的一种颜色。这个简单的 glsl 代码可以使用所有 glsl 内置的函数,例如 mix、cos、texture2D 等。components
可以定义 6 个属性:
名称 | 默认值 | 描述 |
---|---|---|
diffuse |
'vec3(0.0)' |
漫反射颜色,即物体的基本颜色 |
specular |
'0.0' |
镜面反射,定义的是单方向反射光强度 |
shininess |
'1.0' |
镜面反射的清晰度,这个值越大会出现更小的高光光斑 |
normal |
法线,默认无法线 | |
emission |
'vec3(0.0)' |
自发光,默认不发光 |
alpha |
'1.0' |
不透明度,0.0 是完全透明,1.0 是不透明。 |
3.2. 源代码
components
还有一个更强大而灵活的选择是 glsl 源代码,通过 glsl 的方式修改材质。这个途径将设置的 glsl 代码传递到 czm_getMaterial
函数,这个函数执行后返回材质的 components:
struct czm_materialInput
{
float s;
vec2 st;
vec3 str;
mat3 tangentToEyeMatrix;
vec3 positionToEyeEC;
vec3 normalEC;
};
struct czm_material
{
vec3 diffuse;
float specular;
float shininess;
vec3 normal;
vec3 emission;
float alpha;
};
czm_material czm_getMaterial(czm_materialInput materialInput);
默认情况下,材质的默认值会被返回:
czm_material czm_getMaterial(czm_materialInput materialInput)
{
return czm_getDefaultMaterial(materialInput);
}
这个时候的 fabric 对象是:
let fabric = {
components: {
source: `czm_material czm_getMaterial(czm_materialInput materialInput) { return czm_getDefaultMaterial(materialInput); }`
}
}
上面修改了漫反射和镜面反射的例子可以通过 glsl 改写为:
let fabric = {
source: `czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material m = czm_getDefaultMaterial(materialInput);
m.diffuse = vec3(0.5);
m.specular = 0.5;
return m;
}`
}
使用 glsl 代替 components 虽然看起来代码比较冗长,但是提供了灵活性。
如果不是有特别的需求,使用 components 属性指定材质的各种因子就可以了。但是,不管是哪一种,在这些 glsl 代码中,都是可以直接使用 glsl 的内置函数和 Cesium 预定义的 函数、结构体、常量的。
3.3. 输入
materialInput
变量在 source 和 components 中均可以使用,在 glsl 代码的定义中,这个变量是 czm_materialInput
结构体有如下字段:
名称 | 类型 | 描述 |
---|---|---|
s |
float |
一维纹理坐标 |
st |
vec2 |
二维纹理坐标 |
str |
vec3 |
三维纹理坐标,三维纹理的二维部分不一定就是二维纹理坐标,切记。例如,在一个椭球几何中,s可能就是从下到上,st可能是经纬度,str三维纹理就是包围盒的三轴方向。 |
tangentToEyeMatrix |
mat3 |
用于法线贴图、凹凸贴图的转换矩阵,转换切线空间坐标到视图坐标 |
positionToEyeEC |
vec3 |
从 fragment 到 视图空间坐标的向量(不知道这个 fragment 说的是什么),用于反射和折射等。向量的长度是 fragment 到视图(相机)的距离。 |
normalEC |
vec3 |
fragment 在视图坐标中的法线(已归一化),作用于凹凸贴图、反射、折射等 |
例如可以这么设置来可视化纹理坐标:
let fabric = {
components: {
diffuse: 'vec3(materialInput.st, 0.0)'
}
}
一样的,可以把 diffuse 组件设置为 materialInput.normalEC
来可视化法线。
除了 materialInput
这个传入的参数,还可以访问 Cesium 提供的 uniform 变量。
例如,可以通过一个 color uniform 去设置 diffuse 组件和 alpha 组件,来创建自己的 Color 材质:
let fabric = {
type: 'MyColor',
uniforms: {
color: new Color(1.0, 0.0, 0.0, 1.0)
},
components: {
diffuse: 'color.rgb',
alpha: 'color.a'
}
}
在 fabric 中,glsl 中的 uniform 变量、new Cesium.Material()
和 Cesium.Material.fromType()
返回的 js 对象中的 uniform 变量与 uniforms
属性的子属性(例如这里的 uniforms.color)具有相同的名称。
子属性的值(对于标量来说)或子属性(对于向量来说)即 uniform 的值。
官方这说的什么东西...
下例,通过 image uniform 来实现自定义的 DiffuseMap 材质:
let fabric = {
type: 'OurDiffuseMap',
uniforms: {
image: 'czm_defaultImage'
},
components: {
diffuse: 'texture2D(image, materialInput.st).rgb'
}
}
czm_defaultImage
是 1x1 分辨率的图片,根据上面的说法,这可以从 dataurl 或图片文件中获取,例如:
polygon.appearance.material = Material.fromType('OurDiffuseMap');
polygon.appearance.material.uniforms.image = 'diffuse.png';
还有一个多维数据集的变量:czm_defaultCubeMap
。
支持 glsl 的 uniform 类型,例如 float、vec3、mat4 等。
对于 uniform 数组还不支持,不过在规划中了。
译者注
这段是真够烧脑子的,不知所云。
梳理一下,在 fabric 这个对象中,uniforms 下的所有属性均被 glsl 认作是 uniform 变量,可以直接当变量使用,例如上例中的
texture2D(image, materialInput.st)
中的 image 参数。在这里,规定了 image 这个 uniform 的类型是 Cesium 内置的
czm_defaultImage
结构体类型。
3.4. 复合材质
到现在为止,可以使用内置的材质或者通过指定 fabric 中的 components 属性(或直接使用 glsl 源代码)来创建材质对象。
还可以通过现有材质来创建复合类型的材质。
fabric 对象有一个 materials
属性,它的每一个子对象均可以是 fabric 对象,最终即一个材质对象。在 materials
中设置的子一级 fabric,可以在最顶级的 fabric 的 components、source 中引用。例如,现在将 DiffuseMap 材质 和 SpecularMap 材质创建一个复合材质:
let rootFabric = {
type: 'OurMappedPlastic',
materials: {
diffuseMaterial: {
type: 'DiffuseMap' // 基本类型中的一种
},
specularMaterial: {
type: 'SpecularMap'
}
},
components: {
diffuse: 'diffuseMaterial.diffuse',
specular: 'specularMaterial.specular'
}
}
这个 rootFabric 材质拥有两个组件:diffuse(漫反射)和 specular(镜面反射强度),而这两个组件的值是来自 materials 中的两个子 fabric。显然,在 materials
定义的两个子属性的名称,将会在 components 的 glsl 代码中被作为变量使用,直接点出 diffuse 和 specular 字段。
然后就可以像其他材质一样使用它了:
let m = Cesium.Material.fromType('OurMappedPlastic')
polygon.appearance.material = m;
m.materials.diffuseMaterial.uniforms.image = 'diffuseMap.png';
m.materials.specularMaterial.uniforms.image = 'specularMap.png';
4. 语法规定
用了这么多 fabric 对象,其实这个 fabric 对象是有一些规定的。
在 Cesium 的官方打包包中,找到 Documentation/Schemas/Fabric
,就是 fabric 对象的规定。诸如 type
、materials
、uniforms
、components
、source
这几个属性均能找到详细的定义。
5. 渲染流水线中的材质
从渲染的角度看,一种材质其实是一个 glsl 函数:czm_getMaterial
。片元着色器需要构造一个 czm_MaterialInput
结构体变量,调用 czm_getMaterial
函数,然后生成 czm_material
结构变量,传递给照明函数来计算片元的颜色。
在 js 中,fabric 对象应该有一个 material
属性。当此属性发生变动时,图元的 update 函数触发,然后将 fabric 材质最终的 glsl 代码与默认的片元着色器代码合并在一起,然后再将 uniform 合并:
const fsSource = this.material.shaderSource + ourFragmentShaderSource;
this._drawUniforms = combine([this._uniforms, this.material._uniforms]);
6. 笔者注
这篇文章较为详细地介绍了 Appearance 对象中的 fabric 属性构成。
fabric 是一个有官方规定如何写的 js 对象,它拥有 5 个属性:
- type
- materials
- source
- components
- uniforms
其中,type 用于声明 fabric 对象最终会生成什么材质,如果是官方内置的,直接用官方内置的(2.1~2.4),否则则创建自定义的材质并缓存。
materials 允许再塞进去子一级的 fabric,构成复合材质。
uniforms 是一些全局变量,例如你可以在这里写一个 myUniformVariable
,然后你就可以在 components 或者 source 的 glsl 代码中用到这个 uniform 变量了。
source 是 glsl 源代码,它主要是对 czm_getMaterial
这个 Cesium 内置的 glsl 函数的实现,返回值是 czm_material
components 是几个基本材质因子的 glsl 代码快捷入口,是 source 的一种简略实现。
创建好 fabric 材质对象后,随之就可以创建 Appearance 对象,与几何实例一起创建 Primitive 了。