三维模型架构(即 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 = loader.components;
const structuralMetadata = components.structuralMetadata;
/* ... */
})
.catch(/* ... */);
/* ... */
}
从而创立一个叫“模型场景图构造”(ModelExperimentalSceneGraph
)的对象,这个对象待会会讲。
进入到 GltfLoader.prototype.load
办法中,会发现它实际上创立了一个 GltfJsonLoader
,这个时候第 1 节中介绍的缓存机制就上场了:
GltfLoader.prototype.load = function () {const gltfJsonLoader = ResourceCache.loadGltfJson(/* ... */);
/* ... */
gltfJsonLoader.promise
.then(/* ... */)
.catch(/* ... */);
}
ResourceCache.loadGltfJson
这个办法中缓存器就派上用场了,首先应用 ResourceCache.get
办法获取缓存池中是否有这个 GltfJsonLoader
,有则间接返回,无则 new 一个新的,并调用 ResourceCache.load
办法作缓存并加载。
相熟 glTF 格局标准的人应该都分明,glTF 格局有一个 JSON 局部作为某个 glTF 模型文件的形容信息,GltfJsonLoader
其实就是加载这部分 JSON 并简略解决成 Cesium 所需的信息的。
ResourceCache.load
实际上是间接调用传进来的某个 ResourceLoader
(此处即 GltfJsonLoader
)的 load
办法,并作了一次缓存。
2.2. GltfJsonLoader 申请并解析 glTF 的 JSON 局部
从 2.2 得悉,ResourceCache.load
执行的是 ResourceLoader
,也即本节关怀的 GltfJsonLoader
原型链上的 load
办法,这个办法的作用就是分状况去解决传进来的参数,在异步的 Promise 链中保留解决的后果,这个后果也就是 glTF 模型的 JSON 局部。
GltfJsonLoader.prototype.load = function () {
this._state = ResourceLoaderState.LOADING;
let processPromise;
if (defined(this._gltfJson)) {processPromise = processGltfJson(this, this._gltfJson);
} else if (defined(this._typedArray)) {processPromise = processGltfTypedArray(this, this._typedArray);
} else {processPromise = loadFromUri(this);
}
const that = this;
return processPromise
.then(function (gltf) {if (that.isDestroyed()) {return;}
that._gltf = gltf;
that._state = ResourceLoaderState.READY;
that._promise.resolve(that);
})
.catch(/* ... */);
}
函数体两头的三个逻辑分支,别离解决了传参为 glTF JavaScript 对象、glb 或 glTF JSON 文件二进制数组、网络地址三种类型的 glTF。
对应的 5 种状况如下:
ModelExperimental.fromGltf({
url: {// glTF JSON 本体},
url: new Uint8Array(glTFJsonArrayBuffer),
url: new Uint8Array(glbArrayBuffer),
url: "path/to/model.glb",
url: "path/to/model.gltf"
})
5 种状况均可,然而个别比拟惯例的还是给网络门路,5 种状况由几个 GltfJsonLoader.js
模块内的函数来解决。
- 第 1 种状况,由
processGltfJson
解决; - 第 2、3 种状况,由
processGltfTypedArray
解决; - 第 4、5 种状况,由
loadFromUri
解决。
这三个函数均为异步函数,返回一个 Promise。而 glTF 的处理过程,Cesium 团队还官网设计了一个 GltfPipeline
目录,位于 Source/Scene
目录下。
loadFromUri
会继续执行 processGltfTypedArray
,进而继续执行 processGltfJson
,processGltfJson
函数就会调用 GltfPipeline
目录下的各种处理函数来应酬灵便多变的 glTF 数据。
我简略地看了一下,大略就是降级 glTF 1.0 版本的数据、补全 glTF 标准中的默认值、加载内嵌为 base64 编码的缓冲数据等操作,返回一个对立的 glTF JSON 对象。
到 processPromise 的 then 为止,glTF 的初步解析就算实现了,相当于买来的食材摘掉了发黄的菜叶,烧洗了五花肉的猪毛,清洁了瓜果的表皮,切掉了不须要的枝干,自身对食材(glTF)还没有开始做任何解决。
2.3. 状态判断
依据 glTF 设计的灵活性,glTF 数据可能存在二级加载的过程,也就是要先获取 glTF JSON,而后才获取这个 JSON 上定义的 Buffer、Image 等信息,所以异步是不可避免的。(甚至有三级加载过程,也就是二级加载到 Buffer、Image 后,仍需异步解码 Draco 缓冲或者压缩纹理数据等)
然而 CesiumJS 又是一个渲染循环程序,所以应用枚举状态值来辅助判断以后帧下,各种 loader 的状态是怎么样的,资源解决到哪一步,就分支去调用哪一步的处理函数。
譬如,GltfLoader.prototype.load
办法中,会对 GltfJsonLoader
的 promise
成员字段(ES6 Promise
类型)进行 then
链式操作,批改 GltfLoader
的状态:
GltfLoader.prototype.load = function () {
/* ... */
const that = this;
this._promise = gltfJsonLoader.promise
.then(function () {
/* ... */
that._state = GltfLoaderState.LOADED;
that._textureState = GltfLoaderState.LOADED;
/* ... */
})
.catch(/* ... */)
/* ... */
}
下面这段简化后的代码,意思就是在 gltfJsonLoader.promise
的 then
中,glTF JSON 局部曾经加载结束,那么此时就能够标记 GltfLoader
的 _state
和 _textureState
为“已加载(但未解决)”,也就是 GltfLoaderState.LOADED
。
你还能够在 Scene/ResourceLoaderState.js
模块中找到 ResourceLoaderState
这个实用于全副 ResourceLoader
的状态枚举。
2.4. glTF 的提早解决机制 – 应用 ES6 Promise
依据 2.3 大节的内容,glTF 有多级加载、解决的过程,Cesium 团队在 ES6 公布之前,用的是 when.js
库提供的 Promise
,当初 1.9x 版本早已换成了原生的 Promise API
,也就是用 Promise 来解决这些异步过程。
留神到 ModelExperimental.js
模块内的 initialize
函数有一段 Promise then 链,是 GltfLoader
(仍以 ModelExperimental.fromGltf
为例)的一个 promise,then 链内接管 GltfLoader
上的各种组件,从而创立出 ModelExperimentalSceneGraph
:
function initialize(model) {
const loader = model._loader;
/* ... */
loader.load();
const loaderPromise = loader.promise.then(function (loader) {
const components = loader.components;
/* ... */
model._sceneGraph = new ModelExperimentalSceneGraph(/* ... */);
/* ... */
});
/* ... */
}
那么,loader.promise
是何方神圣呢?
代码定位到 GltfLoader.prototype.load
办法,它返回的是一个 Promise:
GltfLoader.prototype.load = function () {
/* ... */
const that = this;
let textureProcessPromise;
const processPromise = new Promise(function (resolve, reject) {
textureProcessPromise = new Promise(function (
resolveTextures,
rejectTextures
) {that._process = function (loader, frameState) {/* ... */};
that._processTextures = function (loader, frameState) {/* ... */};
};
}); // endof processPromise
this._promise = gltfJsonLoader.promise
.then(function () {if (that.isDestroyed()) {return;}
that._state = GltfLoaderState.LOADED;
that._textureState = GltfLoaderState.LOADED;
return processPromise;
})
.catch(/* ... */)
/* ... */
return this._promise;
}
this._promise
是 gltfJsonLoader.promise
的 then
链返回的 processPromise
,是一个位于代码稍上方的另一个 Promise
。我感觉,为了逻辑上不那么凌乱,暂到这一步为止,能够先下一个论断:
ModelExperimental 创立模型场景图的
then
链所在的 Promise 对象,追根溯源,其实是GltfLoader.prototype.load
办法外部 new 的一个名为processPromise
的 Promise 对象。便于探讨,无妨设创立场景图这个 Promise 为“A”。
咱们在 ModelExperimental.js
模块内的 initialize
函数内,对“A”这个 Promise 进行 then
链式操作,then
链内收到的 loader
在本大节的背景下,就是 GltfLoader
,所以能力获取到 GltfLoader
上的 components
,这意味着“A”必定能找到一个 resolve()
语句,把 GltfLoader
给 resolve 进来。
果不其然,在 GltfLoader.prototype.load
办法内,processPromise
内,new 了一个 textureProcessPromise
,在这个 textureProcessPromise
内,就找到了 processPromise
的 resolve 语句:
GltfLoader.prototype.load = function () {
/* ... */
const that = this;
let textureProcessPromise;
const processPromise = new Promise(function (resolve, reject) {
textureProcessPromise = new Promise(function (
resolveTextures,
rejectTextures
) {that._process = function (loader, frameState) {
/* ... */
if (loader._state === GltfLoaderState.PROCESSED) {
/* ... */
resolve(loader); // 在这儿
}
};
that._processTextures = function (loader, frameState) {/* ... */};
};
});
/* ... */
}
能够看到,它是在 that._process
这个办法上的 GltfLoaderState.PROCESSED
(处理完毕)状态分支上 resolve
的。
that
就是GltfLoader
自身,_process
是一个初始化GltfLoader
时定义的空函数,直到在此时才会齐全定义。这个办法,实际上是下一大节(2.5)的内容,也即解决由GltfJsonLoader
初步解决的 glTF JSON,产出用于创立ModelExperimentalSceneGraph
的组件。
为什么要层层封装呢?这对于浏览源码的人来说心智累赘略大。
起因就是 glTF 标准的定义,很灵便,有多级加载的可能性。Cesium 团队在逻辑上是这样程序组织 Promise 代码的:
- 先由
GltfJsonLoader
加载、解析 glTF 的 JSON 局部,降级 glTF 版本、补全默认值、解析内嵌缓冲数据后,向下传递这个初步解析的 glTF JSON 对象; - 向下传递是借助
GltfJsonLoader
的一个promise
字段成员,接收者位于GltfLoader.prototype.load
办法中,then
链首先会标记GltfLoader
的两个状态为“已加载”,而后返回 load 办法中创立的一个用于下一步加工操作的processPromise
,这个 Promise 最终 resolve 的值即GltfLoader
自身;而这个processPromise
,是解决 glTF 的 JSON 的,要把 JSON 转换为组件 – 也就是创立ModelExperimentalSceneGraph
的原材料,基于 glTF 数据的特色,这一步解决又要分两步:先解决纹理,再解决其余的数据; - 由此,
processPromise
内只有一个操作,那就是 new 一个textureProcessPromise
,确保材质纹理的解决优先级最高; textureProcessPromise
中补全了GltfLoader
的两个解决办法的定义,即_process
和_processTextures
,前者将会 resolveprocessPromise
,后者将会 resolvetextuerProcessPromise
请留神,此时还未正式执行处理函数,也就是 GltfLoader
的 _process
和 _processTextures
办法。
在同步操作上来说,最外层的 ModelExperimental
曾经由模块内的函数 initialize
走完了,也就是 ModelExperimental
曾经创立进去了,然而因为 glTF 只初步加载了 JSON 局部,所有的资源都还没筹备齐全,还没加工成组件,也就创立不了 ModelExperimentalSceneGraph
,没有这个模型场景图构造对象,也就创立不出 DrawCommand。
然而,到当初为止,GltfJsonLoader
的使命曾经实现,下一步将由 GltfLoader
应用解决好的 glTF JSON 创立模型组件,即 2.5 大节的内容。
这里讲的有点超前,然而这些都是下文的内容,请急躁往下看,我抵赖这部分应用 Promise 的确有点麻烦。
一旦 GltfLoader
的 _process
流程走到了 GltfLoaderState.PROCESSED
,也就是 glTF JSON 全副处理完毕,就意味着组件已创立结束,能够创立 ModelExperimentalSceneGraph
了;而纹理一边则由 _processTextures
办法来实现。
所以说为什么 glTF 应用了提早解决机制呢?是因为根自 ModelExperimental.js
模块内的 initialize
函数曾经执行结束,只实现了第一步:实例化了一个 ModelExperimental
,就算随着工夫推向前,最多能达到的状态也只是 GltfJsonLoader
初步加载解析结束 glTF JSON,不会发动下一步。
那么下一步的 GltfLoader
的 _process
和 _processTextures
由谁执行呢?这里先漏一点,是由 scene.primitives.update()
,也就是场景的更新过程触发的 ModelExperimental.prototype.update
过程来执行的,见本文第 3 节。
2.5. 模型组件创立
祝贺你,2.4 大节算是一个头脑风暴,如果你胜利地看下来了。
这一步在 2.4 大节尾曾经走漏了,实际上就是 GltfLoader
接过了 GltfJsonLoader
的大旗,进一步随场景的更新过程执行 _process
、_processTextures
的过程。
这个过程,将创立出模型组件,也就是 ModelExperimentalSceneGraph
的原材料。
还记得我在后面是怎么形容 GltfJsonLoader
的行为的吗?
本文 2.2 大节
相当于买来的食材摘掉了发黄的菜叶,烧洗了五花肉的猪毛,清洁了瓜果的表皮,切掉了不须要的枝干,自身对食材(glTF)还没有开始做任何解决。
这一步由 GltfLoader
加工进去的模型组件,相当于是把初步解决的食材进行了切割、吸干血水,乃至焯水等正式炒菜前的“前置步骤”。
模型组件,由 Scene/ModelComponents.js
模块定义,组件有如下数种:
Quantization
Attribute
Indices
FeatureIdAttribute
FeatureIdTexture
FeatureIdImplicitRange
MorphTarget
Primitive
Instances
Skin
Node
Scene
AnimatedPropertyType
AnimationSampler
AnimationTarget
AnimationChannel
Animation
Asset
Components
TextureReader
MetallicRoughness
SpecularGlossiness
Material
实际上很靠近 glTF JSON 的各个对象,毕竟只是简略的切割、去血水、焯水。这一步没什么太非凡的操作,因为之前的文章讲过 Scene 是如何更新 Primitive 的,就省去这个前置流程,间接看到 ModelExperimental.prototype.update
办法:
ModelExperimental.prototype.update = function (frameState) {processLoader(this, frameState);
/* ... */
}
function processLoader(model, frameState) {if (!model._resourcesLoaded || !model._texturesLoaded) {model._loader.process(frameState);
}
}
上来第一步就是调用 ResourceLoader 原型上的 process 函数,这里仍以 ModelExperimental.fromGltf
为例,那么应该执行的就是 GltfLoader.prototype.process
:
GltfLoader.prototype.process = function (frameState) {
/* ... */
this._process(this, frameState);
this._processTextures(this, frameState);
};
因为 2.4 大节曾经介绍了这两个解决办法的具体定义地位,咱们间接转到 GltfLoader.prototype.load
办法,找到他们的定义。不难发现,他们外部还是做了状态判断,进行不同状态的逻辑分叉:
that._process = function (loader, frameState) {if (!FeatureDetection.supportsWebP.initialized) {/**/}
if (loader._state === GltfLoaderState.LOADED) {/**/}
if (loader._state === GltfLoaderState.PROCESSING) {/**/}
if (loader._state === GltfLoaderState.PROCESSED) {/**/}
}
that._processTextures = function (loader, frameState) {if (loader._textureState === GltfLoaderState.LOADED) {/**/}
if (loader._textureState === GltfLoaderState.PROCESSING) {/**/}
if (loader._textureState === GltfLoaderState.PROCESSED) {/**/}
}
对 GltfLoader
不同的状态走不同的路线。
生成模型组件的分叉位于 if (loader._state === GltfLoaderState.LOADED)
分支下的 parse
函数调用内。这个 parse
函数,定义在 GltfLoader.js
模块内。
简略过一下这个函数,大部分的解析逻辑扩散在 loadNodes
、loadSkins
、loadAnimations
、loadScene
这几个模块内的函数中,其中 loadNodes
是从 glTF JSON 中的 nodes
成员开始的,通过 meshes
、primitives
,而后是 loadMaterial
、loadVertexAttribute
等齐上阵,把 glTF JSON 中对于几何图形的信息全副拆解进去,生成 ModelComponents
命名空间下的各种组件对象。
在 parse
函数的下半局部,有对额定数据的异步解决 promise 进行并发执行的语句:
Promise.all(readyPromises)
.then(function () {if (loader.isDestroyed()) {return;}
loader._state = GltfLoaderState.PROCESSED;
})
.catch(rejectPromise);
Promise.all(loader._texturesPromises)
.then(function () {if (loader.isDestroyed()) {return;}
loader._textureState = GltfLoaderState.PROCESSED;
})
.catch(rejectTexturesPromise);
是这两个并发操作决定了 GltfLoader
的状态为“处理完毕”的。
而一旦被设为 GltfLoaderState.PROCESSED
,那么在 _process
和 _processTextures
这两个函数中,就会执行 2.4 大节中提及的两个 Promise —— processPromise
和 processTexturesPromise
给 resolve 掉,进行下一步创立场景图构造,也就是 2.6 大节。
2.6. 模型场景图构造的创立
有了 GltfLoader.prototype.process
办法解决进去的各种组件后,就能够进一步创立模型场景图构造对象了,也就是 ModelExperimentalSceneGraph
实例。
相干代码位于 ModelExperimental.js
模块内的 initialize
函数中:
function initialize(model) {
const loader = model._loader;
/* ... */
const loaderPromise = loader.promise.then(function (loader) {
const components = loader.components;
/* ... */
model._sceneGraph = new ModelExperimentalSceneGraph({
model: model,
modelComponents: components,
});
model._resourcesLoaded = true;
});
/* ... */
}
ModelExperimentalSceneGraph
的创立其实比较简单,GltfLoader
曾经把最沉重的解决和解析工作实现了,剩下的工作,就是把食材放进锅里炒熟,出菜即 ModelExperimentalSceneGraph
,它的初始化函数只是把模型组件做了一些简略的解决。
glTF 模型有动态的模型,也有带骨骼、蒙皮动画的模型,恰好是这些动静的模型还须要再一次依据“运行时”来取得以后帧的动态数值,能力交给 WebGL 绘图,负责这部分工作的,就是这个 ModelExperimentalSceneGraph
。这部分在第 3 节中会简略提及,此处省略 ModelExperimental
的更新过程。
2.7. 本节小结
多种格局拼装成组件,这是一次加工的后果。而后组件创立场景图构造,这是二级加工的后果。本节以 glTF 模型为例,穿过层层 Promise 穿插调用,理清了 Cesium 为了兼容性做出的新逻辑。简略的说,能够依次为如下流程(以 glTF 为例):
ModelExperimental.fromGltf
入场,创立ModelExperimental
,执行初始化,创立GltfLoader
、GltfJsonLoader
GltfJsonLoader
后行,初步加载并清洁了 glTF JSONGltfLoader
接过初步解决的 glTF JSON,创立模型组件- 在 Promise 链的起点,创立出
ModelExperimentalSceneGraph
别忘了状态机制和缓存机制的功绩!
3. 模型的更新与 DrawCommand 创立
CesiumJS 没有选用 ES6 的类继承,也没有用原型链继承,ModelExperimental
是一种“似 Primitive(PrimitiveLike)”,它与原生 Primitive
相似,有 update
办法来创立 DrawCommand
。
Scene 的更新过程不赘述,能够参考系列的第 2 篇。此处间接跳转至更新办法:
ModelExperimental.prototype.update = function (frameState) {
/* 长长的更新状态过程 */
buildDrawCommands(this, frameState);
/* ... */
submitDrawCommands(this, frameState);
}
这个过程非常复杂,但 Cesium 团队封装地还挺清晰的,这套模型新架构既要兼容非 glTF 格局的 pnts,还要思考到工夫相干的 glTF 骨骼、蒙皮内置动画。
别忘了,这个 update 办法第一步是调用
GltfLoader.prototype.process
办法,见 2.5 大节。
简略的说,在更新的过程中,绘图指令(DrawCommand
)由 ModelExperimentalSceneGraph.prototype.buildDrawCommands
办法创立,这个办法会遍历场景图构造对象下所有 ModelComponents.Primitive
的状态,最终是由 buildDrawCommands.js
模块应用这些状态创立出 DrawCommand 的。
着色器也有专门的新设计,有趣味的能够去看
Source/Shaders/ModelExperimental
上面为这套模型新架构设计的着色器,你会发现好多“XXXStage.glsl”代码,这是一种可扩大的设计,即为残缺的模型着色器增加两头阶段,实现多种成果。待会在 3.1 大节中还会介绍这些阶段哪来的。
具体一点来说,ModelExperimental.prototype.update
还包含了如下我的项目:
- 自定义着色器更新
- 更新光照贴图
- 点云相干更新
- 有因素表的模型则更新因素表
- 有裁剪面的则更新裁剪面信息
- 创立绘图指令(DrawCommand)
- 更新模型矩阵
- 更新场景图构造对象
- 提交绘图指令到 frameState,结束战斗
具体的过程便不再深刻探讨,还是倡议 有 glTF 标准根底 去浏览这部分源码,会更容易一些。
3.1. 创立 DrawCommand 及一些乏味的设计
既然创立 DrawCommand 是场景图构造对象最重要的使命,那么就看看这个过程有什么乏味的货色:
ModelExperimental.prototype.update = function (frameState) {
/* 长长的更新状态过程 */
buildDrawCommands(this, frameState);
/* ... */
submitDrawCommands(this, frameState);
}
function buildDrawCommands(model, frameState) {if (!model._drawCommandsBuilt) {model.destroyResources();
model._sceneGraph.buildDrawCommands(frameState);
model._drawCommandsBuilt = true;
}
}
function submitDrawCommands(model, frameState) {
/* ... */
if (showModel) {
/* ... */
const drawCommands = model._sceneGraph.getDrawCommands(frameState);
frameState.commandList.push.apply(frameState.commandList, drawCommands);
}
}
也就这些,次要的工作还是在场景图对象上的。
实际上,动态资源能够不思考那么多“场景图对象中的设计”,这些额定的设计次要还是为动静模型思考的。
一个是“模型组件运行时再次封装对象”,另一个是“分阶段”。
前者有好几个类,和几何图形相干的是 ModelExperimentalNode
、ModelExperimentalPrimitive
、ModelExperimentalSkin
等,很显然,这些就是对应模型组件的动态化封装,例如 ModelExperimentalNode
对应的是 ModelComponents.Node
。它们与模型组件对象是共存的:
function ModelExperimentalSceneGraph(options) {
/* ... */
this._components = components;
this._runtimeNodes = [];
this._runtimeSkins = [];
/* ... */
}
这些运行时对象由模块内的初始化函数 initialize
递归遍历创立而来。
“阶段”是什么?你在 Scene/ModelExperimental
目录下能够找到挺多“Stage”类的,它们的作用相当于给“模型组件运行时再次封装对象”减少一些可选的性能,每个“阶段”对象都会随着 buildDrawCommands
函数触发一次解决办法,这样就能影响最终创立进去的 DrawCommand
。
不仅“模型组件运行时再次封装对象”容许有“阶段”,场景图构造对象本人也有:
function ModelExperimentalSceneGraph(options) {
/* ... */
this._pipelineStages = [];
this._updateStages = [];
this.modelPipelineStages = [];
/* ... */
}
在调用 ModelExperimentalSceneGraph.prototype.buildDrawCommands
办法创立 DrawCommand 时,这些“运行时对象”会调用本人原型上的 configurePipeline
办法(如果有),决定以后帧要选用那些阶段影响生成的绘制指令。
以 ModelExperimentalPrimitive
为例,它的可配置的阶段就很多了:
ModelExperimentalPrimitive.prototype.configurePipeline = function (frameState) {
const pipelineStages = this.pipelineStages;
pipelineStages.length = 0;
/* ... */
// Start of pipeline --------------------------------
if (use2D) {pipelineStages.push(SceneMode2DPipelineStage);
}
pipelineStages.push(GeometryPipelineStage);
/* 很长,很长... */
pipelineStages.push(AlphaPipelineStage);
pipelineStages.push(PrimitiveStatisticsPipelineStage);
return;
};
阶段的具体执行,就请读者自行浏览 ModelExperimental.js
模块内的 buildDrawCommands
函数了。
这就是场景图对象的一些辅助设计,不难,目标只是更好地升高耦合,加强这套 API 的可扩展性。
3.2. 可能有帮忙的切入点
这里就长话短说了,有了上述提纲挈领的骨干代码,我想这些更适宜有特定优化或学习需要的读者,自行钻研:
- 剔除优化:能够通过设定
ModelExperimental
的色彩透明度为全透明,或者间接设置show
属性为false
,就能够粗犷地不创立 DrawCommand 了;DrawCommand 自身是会被View
筛选的,参考本系列文章的第 2 篇; - 渲染程序调度:即最终传递给 DrawCommand 的
Pass
枚举值,这些受 3.1 大节中各种阶段处理器的影响,见ModelExperimentalSceneGraph.prototype.buildDrawCommands
办法内的ModelRenderResources
、NodeRenderResources
、PrimitiveRenderResources
的传递处理过程; - 因素表、属性元数据与款式:因素表由属性元数据创立而来,参考
ModelExperimental.js
模块内的createModelFeatureTables
函数(初始化时会判断是否须要调用),会依据属性元数据创立ModelFeatureTable
,属性元数据参考 3DTiles 标准中的属性元数据(Metadata)局部;ModelExperimental
对象是能够利用Cesium3DTileStyle
的,然而款式不能与CustomShader
共存; - 模型渲染统计信息:定义在
ModelExperimental
对象的statistics
成员上,类型是ModelExperimentalStatistics
; - 自定义着色器:随
ModelExperimental
的更新而更新,次要是更新纹理资源。
属性元数据、自定义着色器、光影渲染、裁剪立体、GPUPicking 这些能够成为专题的内容,当前思考再写,本篇次要介绍的是三维模型架构的主线脉络。
4. 本文总结
模型架构的变革,使得源码在解决 3D 格局上具备更弱小的可扩展性、可维护性。CesiumJS 抉择了 glTF 生态,为兼容 glTF 1.0、2.0 做出了许多封装。当然,还保留了 3DTiles 1.0 原生的几种瓦片格局的解析,并正在设计 GeoJSON 为瓦片的解析路径。
整个模型架构,单从各种类的封装角度看,为了对立能在更新时生成 DrawCommand,势必有一个对立的内置数据类封装,也就是 ModelExperimentalSceneGraph
,模型场景图,它由某个 ResourceLoader 异步加载实现的 组件 创立而来,这些组件是各种格局(glTF 1.0/2.0、3DTiles 1.0 瓦片格局、GeoJSON)应用不同的 loader 解析而来,大抵分层如下:
缓存池 + ResourceLoader + 运行时场景图多阶段 的设计使得将来扩大其它格局有了可扩大、高性能的可能,然而其中的一些仓促未优化的局部仍待解决。
兴许有人会拿 ThreeJS 来比拟,然而我认为要在同等条件下能力比拟。ThreeJS 也能加载 glTF,然而它的主库并没有加载、解析的性能,这部分性能拆到 Loader 里去了,满血版的 ThreeJS 预计代码量也挺可观的。CesiumJS 的侧重点与 ThreeJS 不一样,鱼是陈腐,熊掌是硬核,看你怎么选,二者得兼,必然付出微小的代价。
如果说 ModelExperimental
这套架构是 Primitive API
的特定数据格式具象化下层封装,那么下一篇 3DTiles 将是 Cesium 团队对 Web3DGIS 的最大奉献,也是 CesiumJS 中三维数据技术的集大成者。