三维模型架构(即 Scene/ModelExperimental
目录下的模块)有别于旧版模型 API(即 Scene/Model.js
模块为主的一系列处理 glTF 以及处理 3DTiles 点云文件的源码),它重新设计了 CesiumJS 中的场景模型加载、解析、渲染、调度架构,更合理,更强大。
这套新架构专门为 下一代 3DTiles(1.1版本,当前暂时作为 1.0 版本的扩展) 设计,接入了更强大的 glTF 2.0 生态,还向外暴露了 CustomShader API。
ModelExperimental
的尾缀 Experimental
单词即“实验性的”,等待这套架构完善,就会去掉这个尾缀词(截至发文,CesiumJS 版本为 1.95)。
接下来,我想先从这套架构的缓存机制说起。
1. ModelExperimental 的缓存机制
1.1. 缓存池 ResourceCache
缓存机制由两个主管理类 ResourceCache
和 ResourceCacheKey
负责,缓存的可不是 Resource
类实例,而是由 ResourceLoader
这个基类派生出来的 N 多个子类:
ResourceCache
类被设计成一个类似于“静态类”的存在,很多方法都是在这个类身上使用的,而不是 new 一个 ResourceCache
实例,用实例去调用。例如:
ResourceCache.get("somecachekey...") // 使用键名获取缓存的资源
ResourceCache.loadGltfJson({
/* . */
}) // 根据配置对象加载 glTF 的 json
上面提到,ResourceCache
缓存的是各种 ResourceLoader
,实际上为了统计这些 loader 被使用的次数,Cesium 团队还做了一个简单的装饰器模式封装,即使用 CacheEntry
这个在 ResourceCache.js
模块内的私有类:
function CacheEntry(resourceLoader) {
this.referenceCount = 1;
this.resourceLoader = resourceLoader;
}
你可以在 ResourceLoader.js
源码中找到一个静态成员 cacheEntries
:
function ResourceCache() {}
ResourceCache.cacheEntries = {};
它只是一个简单的 JavaScript 对象,key 是字符串,也就是等会要讲的 ResourceCacheKey
部分,值即 CacheEntry
的实例。
在 ResourceCache.load
这个静态方法中可以看到是如何缓存的:
ResourceCache.load = function (options) {
// ...
const cacheKey = resourceLoader.cacheKey;
// ...
if (defined(ResourceCache.cacheEntries[cacheKey])) {
throw new DeveloperError(
`Resource with this cacheKey is already in the cache: ${cacheKey}`
);
}
ResourceCache.cacheEntries[cacheKey] = new CacheEntry(resourceLoader);
resourceLoader.load();
}
设计上,缓存的 loader 只允许 load 一次,之后在取的时候都是使用 ResourceCache.get
方法获得。
1.2. 缓存对象的键设计 ResourceCacheKey
Cesium 团队在键的设计上充分利用了待缓存资源的自身信息,或唯一信息,或 JSON 字符串本身,但是我觉得这有些不妥,较长的字符串会带来较大的内存占用。
ResourceCacheKey
也是一个类似静态类的设计,它有很多个 getXXXKey
的静态方法:
ResourceCacheKey.getSchemaCacheKey // return "external-schema:..."
ResourceCacheKey.getExternalBufferCacheKey // return "external-buffer:..."
ResourceCacheKey.getEmbeddedBufferCacheKey // return "embedded-buffer:..."
ResourceCacheKey.getGltfCacheKey // return "gltf:..."
ResourceCacheKey.getBufferViewCacheKey // return "buffer-view:..."
ResourceCacheKey.getDracoCacheKey // return "draco:..."
ResourceCacheKey.getVertexBufferCacheKey // return "vertex-buffer:..."
ResourceCacheKey.getIndexBufferCacheKey // return "index-buffer:..."
ResourceCacheKey.getImageCacheKey // return "image:..."
ResourceCacheKey.getTextureCacheKey // return "texture:...-sampler-..."
这些方法均返回一个字符串,有兴趣的读者可以自己跟进源码了解它是如何从资源本身的信息“计算”出 key 的。
我认为这里存在优化的可能性,三维场景的资源会非常多,对内存容量是一个不小的要求,减小 key 的内存大小或许能提升内存消耗表现,这需要优秀的软件设计,等待官方团队优化或者大手子提交 PR。(2022年6月)
2. 三维模型的加载与解析
ModelExperimental API
的主要入口就是 ModelExperimental
类,它有几个静态方法可供加载不同的模型资源(glTF/b3dm/i3dm/pnts…):
ModelExperimental.fromGltf = function (options) { /* ... */ }
ModelExperimental.fromB3dm = function (options) { /* ... */ }
ModelExperimental.fromPnts = function (options) { /* ... */ }
ModelExperimental.fromI3dm = function (options) { /* ... */ }
ModelExperimental.fromGeoJson = function (options) { /* ... */ }
均返回一个 ModelExperimental
实例。从这几个方法可以看出,还是兼容了 3DTiles 1.0 中的三个主要瓦片格式的。其中,fromGeoJson
方法是一个尚未完全实现的规范,允许使用 geojson 作为瓦片的内容,有兴趣可以看 CesiumGS/3d-tiles 仓库中的一个提案。
以 glTF 为例,先看示例代码:
import {
ModelExperimental,
Transforms,
Cartesian3
} from 'cesium'
const origin = Cartesian3.fromDegrees(113.5, 22.4)
const modelPrimitive = ModelExperimental.fromGltf({
gltf: "path/to/glb_or_gltf_file",
modelMatrix: Transforms.eastNorthUpToFixedFrame(origin)
})
viewer.scene.primitives.add(modelPrimitive)
以 glTF 模型(文件格式 glb 或 gltf)为例,从流程上来看,创建 ModelExperimental
实例的过程是这样的:
ModelExperimental.fromGltf
new GltfLoader()
new ModelExperimental()
fn initialize
GltfLoader.prototype.load
~promise.then → new ModelExperimentalSceneGraph
大部分的初始化工作是由 GltfLoader
去完成的,现在就进入 GltfLoader
中看看吧。
2.1. GltfLoader 的初步加载
GltfLoader
的 load 方法本身是同步的,但是它里面的过程却是一些异步的写法,使用了 ES6 的 Promise。
GltfLoader
把加载结果允诺给 Promise 成员变量 promise
,可以看到在 ModelExperimental.js
模块内的 initialize
函数中,then 链接收初步加载完毕的各种 glTF 组件:
// ModelExperimental.js
function initialize(model) {
const loader = model._loader;
const resource = model._resource;
loader.load();
loader.promise
.then(function (loader) {
const components = load