前言
最近开始学习 Cesium,首先我在b站找了一个教程 Cesium快速上手(2020/02)_哔哩哔哩_bilibili,发现这个教程并没有讲的特别清楚。《 Cesium 学习笔记 》系列文章,将按照这个教程的顺序,结合cesium源码,对教程内容做一个学习总结和扩展。文章中出现的 Demo链接 和环境都基于 Cesium教程中 的官方案例,安装过程可以参考 Cesium快速上手(2020/02)_哔哩哔哩_bilibili 的第一集。
本篇文章《 Model图元使用讲解 》主要是对 Cesium快速上手(2020/02)_哔哩哔哩_bilibili 的第二集进行一个总结和扩展。
CesiumWidget-Scene结构
讲解 model 图元之前,我觉得有必要对 cesium 的整体设计做一个介绍,以明确 model 在 cesium 设计中所在的位置
CesiumWidget才是核心类
Cesium的api非常多,我们从核心说起。搭建一个三维Web程序所需要的最基本的东西有哪些呢? 我们需要一个div,然后基于此div创建canvas,引入webgl环境。 每个三维渲染引擎都有各自不同的方法或者类来做这件事情,Cesium则是通过CesiumWidget这个类创建。如下所示:
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
var widget = new Cesium.CesiumWidget('cesiumContainer');
CesiumWidget构造时,手里拿了一个叫 cesiumContainer
的字符串,它实际上是一个div的id。这个div好比一个舞台,CesiumWidget有了这个舞台,就开始创建一系列子div,还是canvas对象,据此搭建了整个三维场景。
CesiumWidget内部创建的对象主要有以下几个部分,如图所示:
- clock用来记录时间,毕竟三维场景需要进行动态展示,需要通过时间来确定某一帧的绘制内容
- container则是构造函数的参数,也就是传入的div,这里记录一下
- canvas则是在container上构建的Canvas类的对象,可以据此获取WebGL绘制的画笔
- screenSpaceEventHandler则是对Canvas对象上各种鼠标的交互事件的封装,方便传递给三维场景,三维场景干之后可以据此改变相机姿态等
- scene则承载着整个三维场景中的对象
Scene中装载了所有的三维对象
Scene中有一些内置的图元对象:地球(globe)、skyBox(天空盒)、sun(太阳)、moon(月亮)等等; 另外还有两个用来由用户自行控制存放对象的数组:primitives和groundPrimitives。
图元:Cesium用来绘制三维对象的一个独立的结构
图元是Cesium用来绘制三维对象的一个独立的结构。图元类有:Globe、Model、Primitive、BillboardCollection、ViewportQuad等。
Globe绘制的是全球地形,它需要两个东西,一个是地形高程信息,另外一个是影像图层,也就是地球的表皮。影像图层可以叠加多个,但是地形高程只能有一个。整个地形的绘制也是渐进式的,即视线看到的地方才会去调度相关的地形高程信息,找到对应位置的地形影像贴上。然而Globe只是一个图元。由此可见一个图元可以相当复杂。
需要注意的地方:
- 图元没有基类,但是所有的图元都会有update函数
- Primitive类直译过来是图元的意思,但是它不是基类,只是图元的一种,起作用是用来绘制各种几何体
- 图元是一类对象绘制的集合,可以包含多个WebGL的drawcall。
createModel 讲解
createModel 示例讲解链接地址 ,注意是开发模式的示例 development/3D Models - Cesium Sandcastle
先上 demo 中 createModel函数 代码
function createModel(url, height, heading, pitch, roll) {
height = Cesium.defaultValue(height, 0.0); // 如果 height 已定义,则返回第一个参数,否则返回第二个参数。
heading = Cesium.defaultValue(heading, 0.0);
pitch = Cesium.defaultValue(pitch, 0.0);
roll = Cesium.defaultValue(roll, 0.0);
const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
const origin = Cesium.Cartesian3.fromDegrees(
-123.0744619,
44.0503706,
height
);
const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
origin,
hpr
);
// 在添加model之前,在场景scene上去除所有的图元
scene.primitives.removeAll();
model = scene.primitives.add(
Cesium.Model.fromGltf({
url: url, // 表示 gltf 模型 存放的位置
modelMatrix: modelMatrix,
minimumPixelSize: 128, // 进行缩放的过程中模型的所占的最小像素数,保证不管地球缩放到多小,依然能看得见model
})
);
// Cesium.Model.fromGltf方法异步载入了glTF 以及它的一些外部资源文件,完全载入(响应了 readyPromise)之后进行了渲染。
model.readyPromise
.then(function (model) {
model.color = Cesium.Color.fromAlpha(
getColor(viewModel.color),
Number(viewModel.alpha)
);
model.colorBlendMode = getColorBlendMode(
viewModel.colorBlendMode
);
model.colorBlendAmount = viewModel.colorBlendAmount;
model.activeAnimations.addAll({
multiplier: 0.5,
loop: Cesium.ModelAnimationLoop.REPEAT,
});
const camera = viewer.camera;
const controller = scene.screenSpaceCameraController;
const r = 2.0 * Math.max(model.boundingSphere.radius, camera.frustum.near);
controller.minimumZoomDistance = r * 0.5;
const center = Cesium.Matrix4.multiplyByPoint(
model.modelMatrix, // Matrix4
model.boundingSphere.center,
new Cesium.Cartesian3()
);
const heading = Cesium.Math.toRadians(230.0);
const pitch = Cesium.Math.toRadians(-20.0);
camera.lookAt(
center,
new Cesium.HeadingPitchRange(heading, pitch, r * 2.0)
);
})
.catch(function (error) {
window.alert(error);
});
}
const height = 100.0;
const heading = 0.0;
const pitch = Cesium.Math.toRadians(10.0);
const roll = Cesium.Math.toRadians(-20.0);
createModel(
"../../SampleData/models/CesiumAir/Cesium_Air.glb",
height,
heading,
pitch,
roll
);
new Cesium.HeadingPitchRoll ( heading, pitch, roll )
1. 参数 :heading, pitch, roll
这个 development/3D Models - Cesium Sandcastle 中的3D模型是一个飞机模型,所以这三个参数可以形象地解释为相对于飞机自身的三个旋转角度:航向、俯仰和滚动。
其实这三个参数其实是以模型自身作为原点的坐标系的,不和其他的参照物有关,只是控制model的姿态方向。参考 理解几何旋转与欧拉角 。旋转的原则是右手原则。
- heading: 表示飞机左右摇头的角度,改变航向。 在3维坐标系(Cartesian3)中,heading 是围绕负z轴的旋转。
- pitch:表示飞机上下抬头低头角度。 在3维坐标系(Cartesian3)中,pitch 是绕负y轴旋转。
- Roll:表示相对于视线方向,从左到右旋转的角度。 在3维坐标系(Cartesian3)中,Roll 是围绕正x轴的旋转
2. 示例图片
- heading = 0; pitch = 0 ; roll = 0。 X轴为红色,指向东; Y轴是绿色,指向北; Z轴是蓝色,指向天空。
// X轴为红色,指向东; Y轴是绿色,指向北; Z轴是蓝色,指向天空。
const heading = 0; // 弧度
const pitch = Cesium.Math.toRadians(0); // 将度数转换为弧度
const roll = Cesium.Math.toRadians(0); // 将度数转换为弧度
- heading = π / 2 ; pitch = 0 ; roll = 0 ; heading 是围绕负z轴的旋转 90°
const heading = 1.57; // π/2, 弧度为单位
const pitch = Cesium.Math.toRadians(0);
const roll = Cesium.Math.toRadians(0);
- heading = 0 ; pitch = π / 2 ; roll = 0 ; pitch 是围绕负 y 轴的旋转 90°
const heading = 0;
const pitch = Cesium.Math.toRadians(90); // π/2, 角度换算为弧度
const roll = Cesium.Math.toRadians(0);
- heading = 0 ; pitch = 0 ; roll = π / 2 ; pitch 是围绕正 x 轴的旋转 90°
const heading = 0;
const pitch = Cesium.Math.toRadians(0);
const roll = Cesium.Math.toRadians(90); // π/2, 角度换算为弧度
new Cesium.Cartesian3.fromDegrees( longitude, latitude, height )
1. 前置知识: 笛卡尔空间直角坐标系(Cartesian3)
我们可以在以上源码段中看到这么一行代码
const origin = Cesium.Cartesian3.fromDegrees ( -123.0744619 , 44.0503706 , 100);
通过chrome,我们可以看到origin返回的是一个含有x,y,z三个属性的 Cartesian3 对象。
要明白这行代码,首先要理解 cesium 中的笛卡尔空间直角坐标系(Cartesian3)
Cesium中的世界坐标系也叫笛卡尔坐标系,也即地球固连坐标系(Earth Fiexed),如下图所示
Cartesian3 以地球的圆心为原点,正X轴穿过本初子午线,正Z轴就穿过北极的中心。这样,我们就可以通过一个3维坐标来确定一个模型在地球上(在地面或者是上空)的任何一个位置。
2. Cesium.Cartesian3.fromDegrees 的作用
其实用一句话概括就是“ 将一个模型的位置(经纬度+距离地面的高度)转化为其在笛卡尔空间直角坐标系中的坐标 ”
在 Cesium.Cartesian3.fromDegrees 中,我们传入的是这个模型所在地球位置的经纬度,比如上述那行代码显式该模型在西经123.07,北纬44.05的位置,且距离地面是100米。
经过fromDegrees的转化,就变成了一个Cartesian3对象,显示该对象在 Cesium 中的世界坐标系的坐标是(-2507720.95, -3850586.72, 4415592.36)
Cesium.Transforms.headingPitchRollToFixedFrame ( origin, hpr )
Cesium.Transforms.headingPitchRollToFixedFrame(origin,hpr)
返回的是一个 Matrix4。
1. 前置知识: Matrix4
一个模型的平移变换和旋转变换都可以由一个4x4矩阵矩阵的表示,以旋转变换举例,可以借下图来理解 Matrix4。
2. cesium 中的 Matrix4
文档中是这么描述该Matrix4的: 'Get the transform from local heading-pitch-roll at cartographic (0.0, 0.0) to Earth's fixed frame.'
我的理解是 origin 表示的是 model 在地球中的位置,hpr 表示的是 model 自身的姿态,从而得到 Matrix4 来表示 model 在地球上的最终姿态。
Cesium.Model.fromGltf( {options} )
1. fromGltf 的作用
- 从 glTF asset 创建模型。当模型准备好渲染时,即下载外部二进制文件、图像和着色器文件并创建 WebGL 资源时,Model.readyPromise 被解析。
- 该模型可以是具有 .gltf 扩展名的传统 glTF asset或使用 .glb 扩展名的二进制 glTF。
options 的详细配置可见 Model - Cesium Documentation
2. fromGltf 的异步加载和缓存
- Cesium.Model.fromGltf方法异步载入了glTF 以及它的一些外部资源文件,完全载入(响应了 readyPromise)之后进行了渲染。
- fromGltf提供了缓存机制, 如果模型id和cache key 一致,则调用缓存中的模型。
3. fromGltf 源码
Model.fromGltf = function (options) {
//>>includeStart('debug', pragmas.debug);
if (!defined(options) || !defined(options.url)) {
throw new DeveloperError("options.url is required");
}
//>>includeEnd('debug');
const url = options.url;
options = clone(options);
// Create resource for the model file
const modelResource = Resource.createIfNeeded(url);
// Setup basePath to get dependent files
const basePath = defaultValue(options.basePath, modelResource.clone());
const resource = Resource.createIfNeeded(basePath);
// If no cache key is provided, use a GUID.
// Check using a URI to GUID dictionary that we have not already added this model.
let cacheKey = defaultValue(
options.cacheKey,
uriToGuid[getAbsoluteUri(modelResource.url)]
);
if (!defined(cacheKey)) {
cacheKey = createGuid();
uriToGuid[getAbsoluteUri(modelResource.url)] = cacheKey;
}
if (defined(options.basePath) && !defined(options.cacheKey)) {
cacheKey += resource.url;
}
options.cacheKey = cacheKey;
options.basePath = resource;
const model = new Model(options);
let cachedGltf = gltfCache[cacheKey];
if (!defined(cachedGltf)) {
cachedGltf = new CachedGltf({
ready: false,
});
cachedGltf.count = 1;
cachedGltf.modelsToLoad.push(model);
setCachedGltf(model, cachedGltf);
gltfCache[cacheKey] = cachedGltf;
// Add Accept header if we need it
if (!defined(modelResource.headers.Accept)) {
modelResource.headers.Accept = defaultModelAccept;
}
modelResource
.fetchArrayBuffer()
.then(function (arrayBuffer) {
const array = new Uint8Array(arrayBuffer);
if (containsGltfMagic(array)) {
// Load binary glTF
const parsedGltf = parseGlb(array);
cachedGltf.makeReady(parsedGltf);
} else {
// Load text (JSON) glTF
const json = getJsonFromTypedArray(array);
cachedGltf.makeReady(json);
}
const resourceCredits = model._resourceCredits;
const credits = modelResource.credits;
if (defined(credits)) {
const length = credits.length;
for (let i = 0; i < length; i++) {
resourceCredits.push(credits[i]);
}
}
})
.catch(
ModelUtility.getFailedLoadFunction(model, "model", modelResource.url)
);
} else if (!cachedGltf.ready) {
// Cache hit but the fetchArrayBuffer() or fetchText() request is still pending
++cachedGltf.count;
cachedGltf.modelsToLoad.push(model);
}
// else if the cached glTF is defined and ready, the
// model constructor will pick it up using the cache key.
return model;
};
model.activeAnimations.addAll({ options })
文档中的解释是
- “ model.activeAnimations.addAll:创建具有指定初始特性的动画并将其添加到集合中 ”。
- “ add 函数返回一个 ModelAnimation 类实例 (addAll 返回一个该类的实例数组), 这个类包含了动画的开始、停止、每帧更新的事件。”
但是我个人的理解是可以使用 options 里面的配置,对gltf中的所有自带动画进行重新设置,比如 multiplier 属性的值大于1会提高动画相对于场景时钟速度的播放速度;小于1的值会降低速度, loop 设置为 Cesium.ModelAnimationLoop.REPEAT 可以使得动画进行循环播放。
Models Instancing 讲解
Models Instancing 示例讲解链接地址 ,注意是开发模式的示例 [development/Models Instancing - Cesium Sandcastle](development/3D Models Instancing - Cesium Sandcastle)
在一个scene中创建多个相同模型的方案
scene 要创建1024个飞机模型,因此有两种方案可以选择,一种是使用上文提到的createModel函数执行1024次 cesium.Model.fromGltf。另外一种是使用 new Cesium.ModelInstanceCollection进行一次性绘制
1. createModel
传入的参数instances是一个含有1024个 Matrix4 对象的数组
function createModels(instances) {
const points = [];
let model;
const length = instances.length; // 1024
for (let i = 0; i < length; ++i) {
const instance = instances[i];
const modelMatrix = instance.modelMatrix;
const translation = new Cesium.Cartesian3();
Cesium.Matrix4.getTranslation(modelMatrix, translation);
points.push(translation);
model = scene.primitives.add(
Cesium.Model.fromGltf({
url: url,
modelMatrix: modelMatrix,
})
);
model.readyPromise
.then(function (model) {
// Play and loop all animations at half-speed
model.activeAnimations.addAll({
multiplier: 0.5,
loop: Cesium.ModelAnimationLoop.REPEAT,
});
})
.catch(function (error) {
window.alert(error);
});
}
2. ModelInstanceCollection
function createCollection(instances) {
const collection = scene.primitives.add(
new Cesium.ModelInstanceCollection({
url: url,
instances: instances,
})
);
collection.readyPromise
.then(function (collection) {
// Play and loop all animations at half-speed
collection.activeAnimations.addAll({
multiplier: 0.5,
loop: Cesium.ModelAnimationLoop.REPEAT,
});
orientCamera(
collection._boundingSphere.center,
collection._boundingSphere.radius
);
})
.catch(function (error) {
window.alert(error);
});
}
ModelInstanceCollection 的性能表现
以下场景即 demo 中一次性绘制 1024 个飞机模型。
1. 重复使用 createModel 的性能表现
2. 重复使用 ModelInstanceCollection 的性能表现
一次把1024个飞机一次绘制出来,降低绘制批次,优化渲染性能。很明显,使用 ModelInstanceCollection 以后,帧率等性能指标有了非常大的提升。
ModelInstance 在 cesium 文档里面没有,是因为ModelInstance.js文档里面有@private关键字,所以自动生成文档的时候没有该关键字。需要打开源码ModelInstanceCollection.js,将@privite修改为@public,然后运行npm run generateDocumentation即可
ModelInstanceCollection 的原理
ModelInstanceCollection 是一个3D模型实例集合。所有实例都引用相同的底层模型,但是每个实例都有独特的属性,如Matrix、选择id等。
ModelInstanceCollection
在内部把每个实例分别构造成一个个ModelInstance
,并赋予不同的instanceId
。它的应用过程也是根据状态来执行的,一开始是NEEDS_LOAD
,在第一次update的时候,会进入LOADING
状态,接着创建Model数据,待到所有的Model资源数据加载完后,就可以进入LOADED
状态了。
而在具体绘制绘制的时候,Cesium是用了一个叫ANGLE_instanced_arrays
的WebGL API扩展。如果需要绘制的对象共享相同的顶点数据、基元计数和类型,则允许多次绘制相同的对象或相似对象组。这个扩展在WebGL1是需要引用的,但在WebGL2中是默认存在的。在绘制的时候调用:
// 与gl.drawElements()的行为相同,只是执行了元素集合的多个实例,并且在每个集合之间执行实例。
instancedArrays.drawElementsInstancedANGLE(mode, count, type, offset, instanceCount);
模型本质上是一组顶点相关数据,每创建一个mesh 相当于多次利用显存中的同一组定点相关数据渲染出多个三维模型的效果,不同的飞机模型虽然是同一组数据,但是可以在GPU着色器中对这组数据进行矩阵变换,来呈现出不同的效果。
在构建Model顶点缓冲数据的时候,若你的模型位置时刻变化,则设置dynamic为true,这个时候有个优化小细节,就是把申请的bufferData保存起来,然后修改这个bufferData就可以了,不必在每帧分配新的内存。
var bufferData = collection._vertexBufferTypedArray;
if (!defined(bufferData)) {
bufferData = new Float32Array(instancesLength * vertexSizeInFloats);
}
if (collection._dynamic) {
// 保留缓冲数据,这样我们就不必在每帧分配新的内存。
collection._vertexBufferTypedArray = bufferData;
}
Model子节点控制
development/3D Models Node Explorer - Cesium Sandcastle
对模型各节点进行控制
每个 gltf asset 都可以由多个 Node 组成
个人理解来说,对模型各节点进行控制实际上就是对每个 node 的 Matrix4 进行改变,从而对模型各节点进行旋转平移等操作
// 改动子节点关键代码,获取子节点的名字model.getNode,改动node.matrix
Cesium.knockout
.getObservable(viewModel, "matrix")
.subscribe(function (newValue) {
const node = model.getNode(viewModel.nodeName); // ModelNode
if (!Cesium.defined(node.originalMatrix)) { // originalMatrix 就是单位矩阵
node.originalMatrix = node.matrix.clone();
}
node.matrix = Cesium.Matrix4.multiply(
node.originalMatrix, // old Matrix4
newValue, // new Matrix4
new Cesium.Matrix4()
);
});
})
.catch(function (error) {
window.alert(error);
});