关于cesium:CesiumJS-PrimitiveAPI-高级着色入门-从参数化几何与-Fabric-材质到着色器-上篇

3次阅读

共计 11713 个字符,预计需要花费 30 分钟才能阅读完成。

Primitive API 还包含 Appearance APIGeometry API 两个次要局部,是 CesiumJS 挡在原生 WebGL 接口之前的最底层图形封装接口(公开的),不公开的最底层接口是 DrawCommand 为主的 Renderer API,DC 对实时渲染管线的技术要求略高,可定制性也高,这篇还是以 Primitive API 为侧重点。

0. 根底

0.1. 坐标系根底

这里的“坐标系”特指 WebGL 图形渲染的坐标系。Primitive API 收到的几何数据,默认没有任何坐标系(即最根本的空间直角坐标),想要挪动到地表感兴趣的中央,须要借助 ENU 转换矩阵,或者把几何顶点的坐标间接设为 EPSG:4978 坐标(即所谓艰深的“世界坐标”)。

ENU 转换矩阵,用道家八卦的说法相似“定中宫”。它能将坐标转换到这样一个 ENU 地表部分坐标系上:

  • 指定一处地表点(经纬度)为坐标原点
  • 以贴地正东方(ENU 中的 E)为正 X 轴
  • 以贴地正北方(ENU 中的 N)为正 Y 轴
  • 以地心到坐标原点的方向(即 ENU 中的 U,up)为正 Z 轴

这样一个 ENU 坐标系上的部分坐标左乘 ENU 转换矩阵后,就能失去规范的 EPSG:4978 世界坐标。

GIS 中的投影坐标、经纬坐标不太实用,须要转换。

0.2. 合并批次

尽管 WebGL 反对实例绘制技术,然而 Primitive API 缩小绘制调用并不是通过这个思路来的,而是尽可能地把 Vertex 数据合并,这个叫做 Batch,也就是“合并批次(并批)”。

在 CesiumJS 的 API 文档中能看到 new Primitive() 时,能够传递一个 GeometryInstance 或者 GeometryInstance 数组,而 GeometryInstance 对象又能复用具体的某个 Geometry 对象,仅在几何的变换位置(通过矩阵表白)、顶点属性(Vertex Attribute)上做差异化。

CesiumJS 会在 WebWorker 中异步地拼装这些几何数据,尽可能一次性发送给底层的 Renderer,以达到尽可能少的 DC。

我没有非常准确地去确认这个并批的概念和 CesiumJS 源码中合并的过程,如有谬误请指出。

1. 参数化几何

这是公开 API 的最惯例用法了,你能够在官网指引文档中学习如何应用参数化几何来创立内置的几何对象:Custom Geometry and Appearance

1.1. 几何类清单

CesiumJS 内置的参数几何有如下数种:

  • 立方体(盒)– BoxGeometry & BoxOutlineGeometry
  • 矩形 – RectangleGeometry & RectangleOutlineGeometry
  • 圆形 – CircleGeometry & CircleOutlineGeometry
  • 线的缓冲区(可设定转角类型和挤出高度)– CorridorGeometry & CorridorOutlineGeometry
  • 圆柱、圆台、圆锥 – CylinderGeometry & CylinderOutlineGeometry
  • 椭圆、椭圆柱 – EllipseGeometry & EllipseOutlineGeometry
  • 椭球面 – EllipsoidGeometry & EllipsoidOutlineGeometry
  • 多边形(可挤出高度)– PolygonGeometry & PolygonOutlineGeometry
  • 多段线 – PolylineGeometry & SimplePolylineGeometry
  • 多段线等径柱体 – PolylineVolumeGeometry & PolylineVolumeOutlineGeometry
  • 球面 – SphereGeometry & SphereOutlineGeometry
  • 墙体 – WallGeometry & WallOutlineGeometry
  • 四棱台(视锥截头体)– FrustumGeometry & FrustumOutlineGeometry
  • 立体 – PlaneGeometry & PlaneOutlineGeometry
  • 共面多边形 – CoplanarPolygonGeometry & CoplanarPolygonOutlineGeometry
  • Esri I3S 专用的几何 – I3SGeometry

这里有两个特地阐明:

  • 除了 I3SGeometry 比拟非凡外,其它的几何对象都有其对应的边线几何对象(边线不是三角网格)
  • CoplanarPolygonGeometryPolygonGeometry 两个 API 很像,然而前者是 2018 年 1.48 起初增加的 API,实用于顶点共面的多边形;不共面的顶点在 PolygonGeometry 中可能会引起解体,但在这个共面多边形 API 不会(只管可能会产生一些不可预测的三角形)。在 PolygonGeometry 呈现三角形显示不失常、不残缺的状况,可思考用这个共面多边形 API;也反对挖洞。

可见 CesiumJS 对参数几何的反对是比拟丰盛的。

1.2. 举例

以下即两个椭球体的实例绘制示例代码:

import {
  EllipsoidGeometry,
  GeometryInstance,
  Matrix4,
  Cartesian3,
  Transforms,
  PerInstanceColorAppearance,
  Color,
  ColorGeometryInstanceAttribute,
  Primitive,
} from 'cesium'


// 只创立一个椭球体几何对象,上面会复用
const ellipsoidGeometry = new EllipsoidGeometry({
  vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
  radii: new Cartesian3(300000.0, 200000.0, 150000.0),
})

// 亮蓝色椭球体绘制实例
const cyanEllipsoidInstance = new GeometryInstance({
  geometry: ellipsoidGeometry,
  modelMatrix: Matrix4.multiplyByTranslation(
    Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(-100.0, 40.0)
    ),
    new Cartesian3(0.0, 0.0, 150000.0),
    new Matrix4()),
  attributes: {color: ColorGeometryInstanceAttribute.fromColor(Color.CYAN),
  },
})

// 橙色椭球体绘制实例
const orangeEllipsoidInstance = new GeometryInstance({
  geometry: ellipsoidGeometry,
  modelMatrix: Matrix4.multiplyByTranslation(
    Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(-100.0, 40.0)
    ),
    new Cartesian3(0.0, 0.0, 450000.0),
    new Matrix4()),
  attributes: {color: ColorGeometryInstanceAttribute.fromColor(Color.ORANGE),
  },
})

scene.primitives.add(
  new Primitive({geometryInstances: [cyanEllipsoidInstance, orangeEllipsoidInstance],
    appearance: new PerInstanceColorAppearance({
      translucent: false,
      closed: true,
    }),
  })
)

代码就不具体解释了,须要有肯定的 WebGL 根底,否则对 vertexFormatattributes 等字段会有些生疏。

如下图所示:

1.3. 纯手搓几何

CesiumJS 的封装能力和 API 设计能力堪称一绝,它给开发者留下了十分多层级的调用办法。除了 1.1、1.2 提到的内置几何体,如果你对 WebGL 的数据格式(VertexBuffer)能纯熟利用的话,你能够应用 Geometry + GeometryAttribute 类本人创立几何体对象,查阅 Geometry 的文档,它提供了一个很简略的例子:

import {Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere} from 'cesium'

const positions = new Float64Array([
  0.0, 0.0, 0.0,
  7500000.0, 0.0, 0.0,
  0.0, 7500000.0, 0.0
])

const geometry = new Geometry({
  attributes: {
    position: new GeometryAttribute({
      componentDatatype: ComponentDatatype.DOUBLE,
      componentsPerAttribute: 3,
      values: positions
    })
  },
  indices: new Uint16Array([0, 1, 1, 2, 2, 0]),
  primitiveType: PrimitiveType.LINES,
  boundingSphere: BoundingSphere.fromVertices(positions)
})

而后就能够持续创立 GeometryInstance,搭配外观、材质对象创立 Primitive 了。

这一个属于高阶用法,实用于有自定义二进制 3D 数据格式能力的读者。

这一步还没有涉及 CesiumJS 的最底层,挡在 WebGL 之前的是一层非公开的 API,叫 DrawCommand,有趣味能够本人钻研。

1.4. * 子线程异步生成几何

有局部参数化几何对象通过一系列逻辑运送后,是要在 WebWorker 内三角化、生成顶点缓冲的。

这大节内容比拟靠近源码解析,不会讲太具体。从 Primitive.prototype.update 办法中模块内函数 loadAsynchronous 看起:

Primitive.prototype.update = function (frameState) {
  /* ... */
  if (
    this._state !== PrimitiveState.COMPLETE &&
    this._state !== PrimitiveState.COMBINED
  ) {if (this.asynchronous) {loadAsynchronous(this, frameState);
    } else {/* ... */}
  }
  /* ... */
}

在这个 loadAsynchronous 函数内,会调度一些 TaskProcessor 对象,这些 TaskProcessor 会通过 WebWorker 的消息传递来实现 Geometry 的 Vertex 创立。这个过程很简单,就不开展了。

如果你感兴趣,关上浏览器的开发者工具,在“源代码”选项卡左侧的“页面”中,能看到一堆“cesiumWorkerBootstrapper”在运行。每一个,背地都是一个内嵌的 requirejs 在调度额定的异步模块,这些异步模块在默默地为主页面生成数据。

2. 应用材质

这一节讲 Primitive API 配套的第二个大类,Appearance + Material API,也叫外观材质 API,它容许开发者为本人的 Primitive 编写着色器。

2.1. 外观 API

CesiumJS 提供了如下几个具体的 Appearance 类:

  • MaterialAppearance – 材质外观,通用型,实用于第 1 节中大部分 Geometry
  • EllipsoidSurfaceAppearance – 上一个的子类,容许用在椭球面上的一些几何,例如 Polygon、Rectangle 等几何类型,这个外观类应用算法来表白局部顶点属性以节约数据大小
  • PerInstanceColorAppearance – 如果每个 GeometryInstance 用的是独自的色彩,能够用这个外观类,在 1.2 的例子中就用到这个类
  • PolylineMaterialAppearance – 应用材质(下一大节)来给有宽度的折线着色
  • PolylineColorAppearance – 应用逐顶点或逐线段来给有宽度的折线着色

外观类有一个形象父类 Appearance(JavaScript 中没有抽象类,CesiumJS 也没有继承,大抵意思,了解记可),上述 5 个均为它的实现类。

通常,为 Primitive 几何着色的主要职责在材质类,然而即便没有材质类,齐全通过 GLSL 代码,设定外观类的顶点着色器和片元着色器(当然,要合规)也是能够实现渲染的。

上面就演示一下用 MaterialAppearance 与着色器代码实现立方体几何对象(BoxGeometry)的着色案例:

import {
  MaterialAppearance,
  Material,
  BoxGeometry,
  Matrix4,
  Cartesian3,
  Transforms,
  GeometryInstance,
  Primitive,
  VertexFormat,
} from 'cesium'

const scene = viewer.scene

// 创立 ENU 转换矩阵后,再基于 ENU 转换矩阵作 Z 轴平移 500000 * 0.5 个单位
const boxModelMatrix = Matrix4.multiplyByTranslation(Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(112.0, 23.0)),
  new Cartesian3(0.0, 0.0, 500000 * 0.5),
  new Matrix4())
// 创立 Geometry 和 Instance
const boxGeometry = BoxGeometry.fromDimensions({
  vertexFormat: VertexFormat.POSITION_NORMAL_AND_ST, // 留神这里,上面要细说
  dimensions: new Cartesian3(400000.0, 300000.0, 500000.0),
})
const boxGeometryInstance = new GeometryInstance({
  geometry: boxGeometry,
  modelMatrix: boxModelMatrix, // 利用 ENU + 平移矩阵
})

// 筹备 fabric shader 材质和外观对象
const shader = `czm_material czm_getMaterial(czm_materialInput materialInput) {czm_material material = czm_getDefaultMaterial(materialInput);
  material.diffuse = vec3(0.8, 0.2, 0.1);
  material.specular = 3.0;
  material.shininess = 0.8;
  material.alpha = 0.6;
  return material;
}`
const appearance = new MaterialAppearance({
  material: new Material({
    fabric: {source: shader}
  }),
})

scene.primitives.add(
  new Primitive({
    geometryInstances: boxGeometryInstance,
    appearance: appearance,
  })
)

而后你就能取得一个 blingbling 的立方块:

留神我在创立 BoxGeometry 时,留了一行正文:

vertexFormat: VertexFormat.POSITION_NORMAL_AND_ST,

应用 WebGL 原生接口的敌人应该晓得这个,这个 VertexFormat 是指定要为参数几何体生成什么 顶点属性 (VertexAttribute)。这里指定的是 POSITION_NORMAL_AND_ST,即生成的 VertexBuffer 中会蕴含顶点的坐标、法线、纹理坐标三个顶点属性。CesiumJS 的教程材料上说过,这个顶点格局参数,几何和外观对象要一一匹配能力兼容。

默认的,所有的 Geometry 对象都不须要传递这个,默认都是 VertexFormat.DEFAULT,也即 VertexFormat.POSITION_NORMAL_AND_ST。无妨设置成这个 POSITION_AND_NORMAL

vertexFormat: VertexFormat.POSITION_AND_NORMAL,

尽管法线影响光照,然而这里只是短少了纹理坐标,盒子就没有 blingbling 的成果了:

具体的着色逻辑不深究,然而足够阐明问题:这个 vertexFormat 会影响几何体的着色成果。

还有一个与外观无关的参数,那就是 new Primitive 时的结构参数 compressVertices,这个值默认是 true,即会依据几何体的 vertexFormat 参数来决定是否压缩 VertexBuffer。

如果设为:

// ...
const boxGeometry = BoxGeometry.fromDimensions({
  vertexFormat: VertexFormat.POSITION_AND_NORMAL,
  dimensions: new Cartesian3(400000.0, 300000.0, 500000.0),
})

// ...

new Primitive({
  geometryInstances: boxGeometryInstance,
  appearance: appearance,
  compressVertices: false
})

即不压缩顶点缓冲,然而 vertexFormat 设置的格局短少了其中某一个,比方这里就短少了纹理坐标,那么就会呈现顶点缓冲和顶点格局不匹配的状况,会呈现报错:

通常,应用 MaterialAppearance 能搭配大多数几何类了,也能够本人应用 Geometry + GeometryAttribute 这两个最根底的类创立出自定义的 Geometry,搭配应用。

只有极少数的状况,须要去动外观对象的两个着色器,这里先不开展,高阶用法会在第 3 节解说。

2.2. 材质 API

CesiumJS 有本人的材质规定,叫做 Fabric 材质,全文参考文档 Fabric,在 2.3、2.4 大节会开展。

先看看间接实例化的参数。应用 new Material({}) 创立一个材质对象,除了 fabric 参数外,还须要这几个参数(有些是可选的):

  • strict: boolean,默认 false,即是否严格查看材质与 uniform、嵌套材质的匹配问题
  • translucent: boolean | (m: Material) => boolean,默认 true,为真则应用此材质的几何体容许有半透明
  • minificationFilter: TextureMinificationFilter,默认 TextureMinificationFilter.LINEAR,采样参数
  • magnificationFilter: TextureMagnificationFilter,默认 TextureMagnificationFilter.LINEAR,采样参数

fabric 参数,则是 Fabric 材质的全部内容,如果不应用内置材质类型要本人写材质的话,就须要认真钻研这个 fabric 对象的参数规定了。

2.3. Fabric 材质初步 – 内置材质、材质缓存与 uniform

如几何、外观 API 一样,Material 类也给予了开发者肯定的内置材质,略像简略工厂模式。只须要应用 Material.fromType() 就能够应用内置的十几种写好着色器的材质。

内置材质也是通过正经的 Fabric 对象创立的,有趣味的能够看源码,所以内置材质也归为 Fabric 内容

列举几种根底材质和几种常见材质:

  • 常见材质 Material.fromType('Color') – 纯色彩
  • 常见材质 Material.fromType('Image') – 一般贴图
  • 根底材质 Material.fromType('DiffuseMap') – 漫反射贴图
  • 根底材质 Material.fromType('NormalMap') – 法线贴图
  • 根底材质 Material.fromType('SpecularMap') – 高光贴图

具体的能够查看 Material 类的 API 文档,文档页面的最顶部就列举了若干种 type 对应的内置材质。fromType() 办法还能够传递第二个参数,第二个参数是这个材质所须要的 uniforms,会利用到着色器对应的 uniform 变量上。

例如,文档中对透明度贴图的 uniform 形容是这样的:

你就能够通过传递这些 uniform 值,来决定着色器应用传入的 image 的哪个 channel,以及要 repeat 的水平:

const alphaMapMaterial = Material.fromType('AlphaMap', {
  image: '绝对于网页运行时的图片门路;网络地址绝对路径;base64 图片', // 对多种图片地址有兼容
  channel: 'a', // 应用图片的 alpha 通道,依据图片的通道数量来填写 glsl 的值,能够是 r、g、b、a 等
  repeat: {
    x: 1,
    y: 1, // 透明度贴图在 x、y 方向的反复次数  
  }
})

当然,Material 类也能够本人创立材质对象,分缓存和一次性应用两种创立办法。

new Material({
  fabric: {
    type: 'MyOwnMaterial',
    // fabric 材质对象的其它参数  
  }
  // ... 其它参数
})
// 缓存后就能够这样用:Material.fromType('MyOwnMaterial', /* uniforms */)

new Material({
  fabric: {// fabric 材质对象的其它参数}
  // ... 其它参数
})

区别就在 fabric.type 参数,只有有 fabric.type,第一次创立就会缓存这个 fabric 材质,第二次就能够应用 fromType() 来拜访缓存的材质了,并且不再须要传递残缺的 fabric 对象,只需传递 type 和新的 uniforms 参数(如果须要更新)即可。

如果不传递 fabric.type 参数,那么创立的材质对象只能在生命周期内应用,CesiumJS 不会缓存,适宜一次性应用。

创立好材质对象后,能够间接批改 uniform 的值实现动静更新成果,例如:

// 赋予一个新材质
primitive.appearance.material = Material.fromType('Image')
// 在某一处动静更新贴图
primitive.appearance.material.uniforms.image = '新贴图的地址'

2.4. Fabric 材质中级(GLSL 表达式、嵌套材质)

Fabric 材质标准容许在创立材质对象时,应用更粗疏的规定。当然能够应用残缺的着色器函数代码,然而为了简略易用,CesiumJS 在“残缺着色器函数”和“JavaScript API”之间还设计了一层“GLSL 表达式”来定制各个 成分组件(components,下文简称成分)

举例:

new Material({
  fabric: {
    type: 'MyComponentsMaterial',
    components: {diffuse: 'vec3(1.0, 0.0, 0.0)',
      specular: '0.1',
      alpha: '0.6',
    }  
  }
})

从这个 components 对象能够看出,这一个材质对象设定了三个成分:

  • diffuse,漫反射色彩,设为了 GLSL 表达式 vec3(1.0, 0.0, 0.0),即纯红色
  • specular,高光强度,设为了 0.1
  • alpha,透明度,设为了 0.6

这些都会合成到残缺的着色器代码的对应重量上。

那么,这个 components 对象容许领有哪些成分呢?这受限制于内置的 GLSL 构造体的成员:

struct czm_material {
  vec3 diffuse;
  float specular;
  float shininess;
  vec3 normal;
  vec3 emission;
  float alpha;
}

也就是说,diffuse(漫反射色彩)、specular(高光强度)、shininess(镜面反射强度)、normal(相机或眼坐标中的法线)、emission(自发光色彩)、alpha(透明度)这 6 个都能够呈现在 components 对象中,其值是字符串, 必须是能够赋予给 GLSL 构造体对应成员的表达式。

什么意思呢?除了下面的举例 diffuse: 'vec3(1.0, 0.0, 0.0)' 外,任意的 GLSL 内置类型、内置函数均可应用,只有是表达式均可,例如 mixcossintantexture2D(GLSL100)、texture(GLSL300)。

举例,如果你在 uniforms 中传递了一个自定义的 image 作为纹理,那么你能够在 components.diffuse 中调用 texture2D 函数对这个 image 变量进行纹理采样:

const someMaterialFabric = {
  type: 'OurDiffuseMap',
  uniforms: {image: 'czm_defaultImage' // 'czm_defaultImage' 是一个内置的 1x1 贴图},
  components: {diffuse: 'texture2D(image, materialInput.st).rgb'
  }
}

其中,texture(image, materialInput.st).rgbimage 就是 uniforms.imagematerialInput.st 是来自输出变量 materialInput 的纹理坐标。至于 materialInput,之后解说 fabric.source 完整版着色器代码的用法时会介绍。

我感觉如果要写一些更简单的表达式,不如间接进阶用法,写残缺的着色器更灵便,components 适宜最简略的表达式。

fabric 对象上曾经介绍了 3 个成员了,即 fabric.typefabric.uniformsfabric.components,那么当初介绍第四个 —— 容许材质组合的 fabric.materials 成员。

侥幸的是,官网的文档有举简略的例子,我就间接抄过来阐明了:

const combineFabric = {
  type: 'MyCombineMaterial',
  materials: {
    diffuseMaterial: {type: 'DiffuseMap'},
    specularMaterial: {type: 'SpecularMap'}
  },
  components: {
    diffuse: 'diffuseMaterial.diffuse',
    specular: 'specularMaterial.specular'
  }
}

materials 中定义的两个子材质 diffuseMaterialspecularMaterial 也是满足 Fabric 标准的,这里间接用了两个内置材质(漫反射贴图材质、高光贴图材质)。定义在 materials 中,而后在 components 和未来要介绍的 fabric.source 着色器残缺代码中都能用了。

例如,这里的 components.diffuse 设为了 diffuseMaterial.diffuse,实际上 diffuseMaterial 就是一个 CesiumJS 内置的 GLSL 构造体变量,在上文提过,构造体为 czm_material

子材质的 uniforms 也和一般材质的一样能够更新:

const m = Material.fromType('MyCombineMaterial')
primitive.appearance.material = m

m.materials.diffuseMaterial.uniforms.image = 'diffuseMap.png'
m.materials.specularMaterial.uniforms.image = 'specularMap.png'

通常不倡议嵌套太深,容易造成性能问题。

中段小结

至此,曾经介绍了 Primitive API 中的两大 API —— Geometry APIAppearance + Material API 的入门和中阶应用,并应用一些简略的代码实例辅助阐明。到这里为止曾经能够使用内置的几何、材质外观来做一些入门的高性能渲染了,然而将来的你肯定不满足于此,那就须要更进阶的用法 —— 残缺的着色器编写,去管制几何体在顶点和片元着色阶段的细节。

受限于篇幅,进阶内容于下一篇解说。

正文完
 0