Cesium原理篇:Material
Shader
⾸先,在本⽂开始前,我们先普及⼀下材质的概念,这⾥推荐,普及材质的内容都是截取⾃该⽹站,我觉得他写的已经够好了。在开始普及概念前,推荐⼀⾸我此刻想到的歌《光---陈粒》。
在真实世界⾥,每个物体会对光产⽣不同的反应。钢看起来⽐陶瓷花瓶更闪闪发光,⼀个⽊头箱⼦不会像钢箱⼦⼀样对光产⽣很强的反射。每个物体对镜⾯⾼光也有不同的反应。有些物体不会散射(Scatter)很多光却会反射(Reflect)很多光,结果看起来就有⼀个较⼩的⾼光点(Highlight),有些物体散射了很多,它们就会产⽣⼀个半径更⼤的⾼光。如果我们想要在OpenGL中模拟多种类型的物体,我们必须为每个物体分别定义材质(Material)属性
我们指定⼀个物体和⼀个光的颜⾊来定义物体的图像输出,并使之结合环境(Ambient)和镜⾯强度(Specular Intensity)元素。当描述物体的时候,我们可以使⽤3种光照元素:环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)、镜⾯光照(Specular
Lighting)定义⼀个材质颜⾊。通过为每个元素指定⼀个颜⾊,我们已经对物体的颜⾊输出有了精密的控制。现在把⼀个镜⾯⾼光元素添加到这三个颜⾊⾥,这是我们需要的所有材质属性:
struct Material
{
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
以上是对材质的⼀个最简单概括,我们下⾯进⼊Cesium的环节。先来看看Cesium在Shader中对Material的定义:
struct czm_material
{
vec3 diffuse;
float specular;
float shininess;
vec3 normal;
vec3 emission;
float alpha;
};
和上⾯给出的结构体⼤致相同,区别是少了环境光ambient,但多了法向量normal,⾃发光emission和alpha,我们带着这个疑问看⼀下Cesium处理材质的⽚段着⾊器:
varying vec3 v_positionEC;
varying vec3 v_normalEC;
void main()
{
vec3 positionToEyeEC = -v_positionEC;
vec3 normalEC = normalize(v_normalEC);
#ifdef FACE_FORWARD
normalEC = faceforward(normalEC, vec3(0.0, 0.0, 1.0), -normalEC);
#endif
czm_materialInput materialInput;
materialInput.positionToEyeEC = positionToEyeEC;
czm_material material = czm_getDefaultMaterial(materialInput);
gl_FragColor = czm_phong(normalize(positionToEyeEC), material);
}
此时的坐标系是以相机为中⼼点,⾸先获取当前点的位置和法向量,通过czm_getMaterial获取默认的⼀个材质对象,gl_FragColor通过czm_phong⽅法得到对应的颜⾊。对于phong,在OpenGL SuperBible⾥⾯有详细的说明,⼤概就是通过material的属性,根据光的位置和光的颜⾊,最终计算出在该点当前环境和⾃⾝材质的影响下对应的颜⾊。我们来看看czm_phong的实现:
vec4 czm_phong(vec3 toEye, czm_material material)
{
float diffuse = czm_private_getLambertDiffuseOfMaterial(vec3(0.0, 0.0, 1.0), material);
if (czm_sceneMode == czm_sceneMode3D) {
diffuse += czm_private_getLambertDiffuseOfMaterial(vec3(0.0, 1.0, 0.0), material);
}
float specular = czm_private_getSpecularOfMaterial(czm_sunDirectionEC, toEye, material) + czm_private_getSpecularOfMaterial(czm_moonDirectionEC, toEye, material);    vec3 materialDiffuse = material.diffuse * 0.5;
vec3 ambient = materialDiffuse;
vec3 color = ambient + ission;
color += materialDiffuse * diffuse;
color += material.specular * specular;
return vec4(color, material.alpha);
}
如上是phong颜⾊计算的算法,我并没有给出getLambertDiffuse和getSpecular的具体代码,都是光的基本物理规律。这⾥要说的是getLambertDiffuse的参数,如果是球⾯物体时,会调⽤czm_private_phong,此时参数为czm_sunDirectionEC,也就是太阳的位置,⽽这⾥认为光源的位置是靠近相机的某⼀个点,另外,环境光ambient默认是反射光的⼀半,这个也说的过去,最后我们看到最终颜⾊的alpha位是material.alpha。
上⾯是Shader中涉及到材质的⼀个最简过程:材质最终影响的是⽚段着⾊器中的颜⾊gl_FragColor,⽽所有czm_开头的都是Cesium内建的⽅法和对象,Cesium已经帮我们提供好了光学模型和计算⽅法,并
不需要我们操⼼,⽽我们要做的,就是指定对应物体的材质属性,通过修改material中的属性值,来影响最终的效果。所以,接下来的问题就是如何指定物体的材质属性。
材质的风格有很多种,形状也不尽相同,线⾯各异,为此,Cesium提供了Material对象,来⽅便我们设置材质。
Fabric
我们先来看看Cesium都提供了哪些内建材质类型,以及如何创建对应的Material,我也是参考的Cesium在github wike上对Fabric的,更详细的内容可以⾃⼰去看。在Cesium中,Fabric是描述材质的⼀种json格式。材质可以很简单,就是对象表⾯的⼀个贴图,也可以是⼀个图案,⽐如条形或棋盘形。
⽐如ImageType类型,Cesium提供了如下两种⽅式来设置:
// ⽅法⼀
primitive.appearance.material = new Cesium.Material({
fabric : {
type : 'Image',
uniforms : {
image : '../images/Cesium_Logo_Color.jpg'
}
}
});
// ⽅法⼆
aterial = Material.fromType('Image');
primitive.appearance..uniforms.image = 'image.png';
Cesium默认提供了⼗⼋个类型:
ColorType
ImageType
DiffuseMapType
AlphaMapType
SpecularMapType
EmissionMapType
BumpMapType
NormalMapType
GridType
StripeType
CheckerboardType
DotType
WaterType
RimLightingType
FadeType
PolylineArrowType
PolylineGlowType
PolylineOutlineType
当然,Cesium⽀持多个Type的叠加效果,如下是DiffuseMap和NormalMap的⼀个叠加,components中指定material中diffuse、specular、normal的映射关系和值:
primitive.appearance.material = new Cesium.Material({
fabric : {
materials : {
applyDiffuseMaterial : {
type : 'DiffuseMap',
uniforms : {
image : '../images/bumpmap.png'
}
},
normalMap : {
type : 'NormalMap',
uniforms : {
image : '../images/normalmap.png',
strength : 0.6
}
}
},
components : {
diffuse : 'diffuseMaterial.diffuse',
specular : 0.01,
normal : 'al'
}
}
});
当然,这些都满⾜不了你的欲望?你也可以⾃定义⼀个⾃⼰的MaterialType,我们先了解Cesium.Material的内部实现后,再来看看⾃定义Material。
Material
⽤户通常只需要指定type,uniforms,components三个属性,构建⼀个Fabric的JSON。这是因为Material在初始化时,会加载上述默认的⼗⼋个类型,⽐如对应的ColorType代码:
Material.ColorType = 'Color';
Material._materialCache.addMaterial(Material.ColorType, {
fabric : {
type : Material.ColorType,
uniforms : {
color : new Color(1.0, 0.0, 0.0, 0.5)
},
components : {
diffuse : 'b',
alpha : 'color.a'
}
},
translucent : function(material) {
return lor.alpha < 1.0;
}
});
// 创建material
polygon.material = Cesium.Material.fromType('Color');
polygon.lor = new Cesium.Color(1.0, 1.0, 0.0, 1.0);
其他的类型也⼤概相同,在初始化的时候已经全部构建。因此,⽤户在执⾏创建时,已经有了⼀个ColorMaterial,只是对⾥⾯的⼀些属性修改为⾃⼰的期望值的过程。我们具体Material.fromType的具体内容:
Material.fromType = function(type, uniforms) {
var material = new Material({
fabric : {
type : type
}
});
return material;
};
function Material(options) {
initializeMaterial(options, this);
if (!defined(Material._pe])) {
Material._pe] = Object.keys(this._uniforms);
}
}
function initializeMaterial(options, result) {
var cachedMaterial = Material._pe);
createMethodDefinition(result);
createUniforms(result);
// translucent
}
initializeMaterial则是其中的重点,⾥⾯有三个关键点:1createMethodDefinition,2createUniforms,3translucent,我们来看看都做了什么function createMethodDefinition(material) {
// 获取components属性
// ColorType:{ diffuse : 'b', alpha : 'color.a'}
var components = material._templateponents;
var source = material._template.source;
if (defined(source)) {
陈粒material.shaderSource += source + '\n';
} else {
material.shaderSource += 'czm_material czm_getMaterial(czm_materialInput materialInput)\n{\n';
material.shaderSource += 'czm_material material = czm_getDefaultMaterial(materialInput);\n';
if (defined(components)) {
for ( var component in components) {
if (components.hasOwnProperty(component)) {
/
/ 根据components中的属性,修改Material中对应属性的获取⽅式
material.shaderSource += 'material.' + component + ' = ' + components[component] + ';\n';
}
}
}
// 封装得到⽚段着⾊器中获取material的函数
material.shaderSource += 'return material;\n}\n';
}
}
如上是Key1的作⽤,拼装出⽚段着⾊器中获取material的函数,如果Type是Color下,获取的函数代码如下:
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
material.diffuse = b;
material.alpha = color.a;
return material;
}
可以对照ColorType的FabricComponents属性,对号⼊座。下⾯就是对Fabric的uniforms属性的解析过程了:createUniforms。这⾥主要有两个作⽤,第⼀,根据uniforms,在⽚源着⾊器中声明对应的uniform变量,⽐如ColorType中uniform对应的color变量,则需要声明该变量,当然cesium做了⼀个特殊的处理,给他们⼀个标号,保证唯⼀:更新后的代码如下:
uniform vec4 color_0;
czm_material czm_getMaterial(czm_materialInput materialInput)
{
czm_material material = czm_getDefaultMaterial(materialInput);
material.diffuse = b;
material.alpha = color_0.a;
return material;
}
第⼆个作⽤是为后⾯的uniformMap做准备,声明了变量了,当然需要准备好该变量的赋值,建⽴好这个key-value的过程,保存到material._uniforms数组中:
function createUniform(material, uniformId) {
// 根据变量的类型,建⽴对应的return value⽅法
if (uniformType === 'sampler2D') {
material._uniforms[newUniformId] = function() {
return material._textures[uniformId];
};
material._updateFunctions.push(createTexture2DUpdateFunction(uniformId));
} else if (uniformType === 'samplerCube') {
material._uniforms[newUniformId] = function() {
return material._textures[uniformId];
};
material._updateFunctions.push(createCubeMapUpdateFunction(uniformId));
} else if (uniformType.indexOf('mat') !== -1) {
var scratchMatrix = new matrixMap[uniformType]();
material._uniforms[newUniformId] = function() {
return matrixMap[uniformType].fromColumnMajorArray(material.uniforms[uniformId], scratchMatrix);
};
} else {
material._uniforms[newUniformId] = function() {
return material.uniforms[uniformId];
};
}
}
createUniforms⽅法后则是对translucent的处理,这个会影响到Pimitive创建RenderState,以及渲染队列的设置。将Fabric中的translucent⽅法保存在material._translucentFunctions中。
Primitive
此时,我们已经创建好⼀个color类型的Material,将其赋给对应的Primitive,代码如下:
primitive.appearance.material = Cesium.Material.fromType('Color');
这⾥出现了⼀个新的的对象:Appearance。这⾥,Material只是负责⽚段着⾊器中,材质部分的代码,⽽Appearance则负责该Primitvie整个Shader的代码,包括顶点着⾊器和⽚段着⾊器两个部分,同时,需要根据Appearance的状态来设置对应的RenderState,可以说Appearance是在Material之上的⼜⼀层封装。⼀共有MaterialAppearance、EllipsoidSurfaceAppearance等六类,⼤同⼩异,每个对象的属性值不同,但逻辑上统⼀有Appearance来负责。我们看如下⼀个Primitive的创建:
var rectangle = scene.primitives.add(new Cesium.Primitive({
geometryInstances : new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(-120.0, 20.0, -60.0, 40.0),
vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT
})
}),
appearance : new Cesium.EllipsoidSurfaceAppearance({
aboveGround : false
})
}));
如上创建的是⼀个EllipsoidSurfaceAppearance,创建时如果没有指定Material,则内部默认采⽤ColorTyoe的材质。当执⾏Primitive.update 时,Appearance的就发挥了⾃⼰的价值:
Primitive.prototype.update = function(frameState) {
createRenderStates(this, context, appearance, twoPasses);
createShaderProgram(this, frameState, appearance);
createCommands(this, appearance, material, translucent, twoPasses, this._colorCommands, this._pickCommands, frameState);
}
⾸先Appearance基类提供了默认的defaultRenderState,也提供了getRenderState的⽅法,如下:
var rs = {
depthTest : {
enabled : true
}
};
if (translucent) {
rs.depthMask = false;
rs.blending = BlendingState.ALPHA_BLEND;
}
if (closed) {
rs.cull = {
enabled : true,
face : CullFace.BACK
};
}
if (defined(existing)) {
rs = combine(existing, rs, true);
}
return rs;
};
RenderState = function() {
var translucent = this.isTranslucent();
var rs = derState, false);
if (translucent) {
rs.depthMask = false;
rs.blending = BlendingState.ALPHA_BLEND;
} else {
rs.depthMask = true;
}
return rs;
};
然后,各个⼦类按照⾃⼰的需要,看是否使⽤基类的⽅法,还是⾃⼰有特殊⽤处,⽐如EllipsoidSurfaceAppearance类:
function EllipsoidSurfaceAppearance(options) {
this._vertexShaderSource = defaultValue(options.vertexShaderSource, EllipsoidSurfaceAppearanceVS);
this._fragmentShaderSource = defaultValue(options.fragmentShaderSource, EllipsoidSurfaceAppearanceFS);
this._renderState = DefaultRenderState(translucent, !aboveGround, derState);
}