Cesium原理篇:glTF

关键字:Cesium glTF WebGL技术

大纲:

1 glTF简介,这是一个什么东西,有哪些特点

2 Cesium如何加载,渲染glTF,逻辑结构和关键技术

3 个人总结,从glTF学习如何设计一个二进制格式,个人想法分享

共计 4000字 | 建议阅读时间 未知

1 glTF简介

之前介绍了Cesium的Property,Material,Batch,GroundPrimitive这些内容,可以说是简单地物和风格的解决思路。当Cesium把这些技术点整合起来,我们便具备了渲染模型的威力。也就是今天要讲的glTF模型渲染。

glTF的全称是GL传输格式,是一种针对GL(WebGL,OpenGL ES以及OpenGL)接口的运行时资产(asset)。在3D内容的传输和加载中,glTF通过提供一种高效,易扩展,可协作的格式,填补了3D建模工具和现代GL应用之间的空白。github上有对该数据规范的详细介绍,春节期间我翻译了其中的核心部分,有兴趣的可以了解

三维模型的格式这么多,为什么不用现成了,而是非要自己重新定义一个规范,原文中Patrick很诚实详尽的做了解释。简单说,目前主流的三维模型主要的特点在于数据制作上,在Web传输和解析上无法满足需求,而glTF的特点就是传输和解析的高效。首先,二进制的传输方式最为高效,也就是ArrayBuffer形式,但二进制的解析则很繁琐,也很容易出错。如何提高解析效率呢,就如同云盘的秒传功能,不解析,或减少解析是最有效的解析方式, 要做到这一点,则要求显卡,WebGL能够直接加载该数据结构。

上图是glTF的一个大概结构,分为四大块,最上面的json是一个表述,描述该模型的节点层级,材质,相机,动画等相关逻辑结构,bin则对应这些对象的具体数据信息,glsl是对该模型渲染的着色器,针对该模型的数据信息,给出渲染“配方”,当然还有纹理内容。大块内容可以以Base64的编码内迁到文件中,方便拷贝和加载,也可以以URI的外链方式,侧重重用性。

下图是json中包含的描述信息,内容详细,比如mesh,纹理,蒙皮和动画,定义了accessor的访问器规则,同时还给出了相机,节点这些场景管理的信息。充分体现了glTF规范设计的强大,让我想到了一句话:“解决问题固然重要,但通过设计避免问题则更胜一筹”。可以说,该规范是对复杂三维模型的一个很不错的抽象,考虑的很充分,之间的接口定义也很规范,如果你也想设计一个二进制数据规范的话,这是一个很好的学习,借鉴范本。

2 glTF渲染

东西再好,光说不练假把式。设计好了,只是一个开始而不是完结,还需要持续的推广和应用。这年头酒香也怕巷子深,伯牙难觅钟子期的画面有没有。下面我们来看看glTF是如何渲染模型的,talk is cheap and show me the code~

加载&渲染

var entity = viewer.entities.add({
    name : url,
    position : position,
    orientation : orientation,
    model : {
        uri : url,
        minimumPixelSize : 128,
        maximumScale : 20000
    }
});

var model = scene.primitives.add(Cesium.Model.fromGltf({
    url : ‘./duck/duck.gltf‘
}));

// 内部通过该方法来解析JSON对象,获取表述信息和具体的数据内容
function parseBinaryGltfHeader(uint8Array) {
    var json = getStringFromTypedArray(uint8Array, sceneOffset, sceneLength);
    return {
        glTF: JSON.parse(json),
        binaryOffset: binOffset
    };
}

如上是加载glTF的过程,也是提供两种方式,一种是以Entity的方式,一种是以Primitive的方式,消费数码相机(前者)和单反相机(后者)的差别。同时,Cesium对Model的渲染也是基于状态的更新的,这个和地球,Entity的渲染思路是一致的。Model有三个状态,加载(NEEDS_LOAD),解析(LOADING),和结束(LOADED)。在不同状态下做该做的事,各司其职,互不干涉。

Model.prototype.update = function(frameState) {
    // Key 1 解析json对象中的各个对象
    // 比如是否有动画,数据视图具体情况,是否有扩展属性等
    if ((this._state === ModelState.NEEDS_LOAD) && defined(this.gltf)) {
        parse(this);
        this._state = ModelState.LOADING;
    }

    // Key 2 解析后,对需要调用的内容赋值
    // 比如顶点数据和索引,材质,纹理等封装,动画,Runtime封装到对应的RuntimeNode
    if (this._state === ModelState.LOADING) {
        createResources(this, frameState);
        this._state = ModelState.LOADED;
    }

    // Key 3 更新动态属性,传递到对应的着色器参数中
    // 比如动画,骨骼等,更新对应变量的节点矩阵,重新梳理节点层级对应的矩阵等参数
    if ((show && this._state === ModelState.LOADED) || justLoaded) {
        updateNodeHierarchyModelMatrix(this, modelTransformChanged, justLoaded, frameState.mapProjection);
    }

    // 渲染队列
    if (show && !this._ignoreCommands) {
         var commandList = frameState.commandList;
         // ……
     }
}

如上是Model的状态更新函数,每一个状态只专注于自己的业务,当处理完后完成状态的更新。update实现实时更新和渲染。这里以读一本书为例来描述这个过程,首先,我们先解析glTF 的头信息,也就是json对象,了解该模型的大概结构,这就好比一本书的目录,当我们对一本书感兴趣的时候,都会先看看目录,了解一个大概;接着,我们开始解析glTF数据,将每一个结构中的数据解析赋值,这是最复杂,也是最关键的过程之一,我们开始逐章节的阅读这本书;最后,我们彻底解析完该数据,则构造对应的DrawCommand,添加到渲染队列中;如果该数据中包含一些时态数据,比如动画,蒙皮等,则每一帧都要动态的调整。这就是update中主要的四个状态和逻辑,完成该模型的渲染。下面我们详细介绍这个过程中三个重要的部分。

BufferView&Accessor

如图,红框部分,从下往上看。Buffer缓存是一个二进制的数据块,是几何对象,动画和蒙皮等数据信息的组合,在json中申明了这个数据块的类型arraybuffer和长度。BufferView,缓存视图,是Buffer的子集,如果Buffer是一本书的内容,那么BufferView就是一个目录,将这本书划分成章节,并表示该章节的起始页和长度。缓存和缓存视图并不包含类型信息。他们只是简单定义从文件中取出的原始数据,并不知道这些数据到底有什么涵义和结构。glTF文件中的对象(网格,蒙皮,动画)都不会直接访问缓存或缓存视图,而是通过Accessor访问器,这样我们拿到这块数据后,知道这块数据是vec4,float还是其他类型。

Mesh

如上,有了访问的规范,我们还得知道一个几何对象的逻辑结构,就好比拼图游戏,我们能拿到一块块拼图,心中还要有一个轮廓,能把这些拼图拼成一个完整的图像。下面我们来看看Mesh这张图是有哪些部分构成的,一切的一切还是从上方图的红框开始。

该Mesh可以有多个Primitive组成,每个图元有attribute顶点数据,indices顶点索引,mode类型为triangles,还有material材质,这些内容我们已经在之前的章节介绍过,不知道你还给我多少。我们再看material对象,里面用到了technique,其他的都是具体的光照模型的参数值,稍微特殊的是diffuse,是一张纹理。technique里面封装了着色器需要的参数,包括attribute和uniform,以及GL状态states,对应的着色器代码program,还有shaders,texture纹理的封装等,这些对象的值是一个accessor,进而获取对应的值。。这些对象我们之前都详细介绍过,我们顺藤摸瓜,算是对之前内容的温习,并串联成一个完整体系。可以说,里面的技术点都和以前的内容一样,glTF定义了他们之间交互的规范,将他们封装为一个整体。

Scene&Animation

在很多应用中,只是从一个建模数据包中带出单一对象,这并不充分。因此glTF还包含整个场景的关系,包括节点,变换矩阵,变换的层级关系,网格,材质,相机和动画,试图保存所有信息。这是一个场景树的逻辑,算是glTF的一个优化。如上图,该Scene中有三个node,其实Cesium_Air节点对应的mesh名字为Geometry-mesh090,他还有两个子节点。

当然,Cesium内部提供了动画的解析(_runtime),在createRuntimeAnimations方法中实现,详细的自己来看。其中包括TIME计时器,samplers插值方式,所对应的动画节点和具体的属性(比如rotation)。这样每一帧会更新对应的值。

3 总结

如上是glTF的一个介绍,下面来谈几点个人的想法。

必要性

设计一个二进制文件的风险很大的,多数情况下会是一个失败的产品。所以,当你觉得你需要一个新的数据格式时,你最好的选择就是回家睡觉,早上起来想想是否还有这种冲动。如果时间久了,冲动还在,再理性的衡量也不迟。《Unix编程艺术》里面概括了两个衡量点:时效性和数据量。当已有的数据格式无法满足你对这两点的需要时,或许你真的有充分的理由来设计一个新的数据格式了。

设计一个新的数据格式是一件很有挑战的事情,而且由于自身的局限性,剧情很可能是这样的,你设计完了,很好的解决了你的问题,你觉得很棒,但不久的未来,随着应用的推广,需求的增加,现有的数据格式无法满足业务的多样性,有可能是你考虑不充分,有可能是过分需求,这些都是未知的风险,让你陷入两难,增加版本号,数据规范升级,版本多了会很混乱,也会出现旧版本读取新数据这种无法解决的隐患。微调的话,则会弄脏现有的数据规范。慢慢的,时间证明了它是多么的失败。

所以,这个人经验一定要丰富,谨慎,能够做决定的人越少越好,不仅着眼于当前要解决的问题,还要综合考虑通用性。但这又是一个困惑,也要控制它的应用范围,随着硬件性能的提高,避免过渡设计和优化。比如glTF提供了扩展,提供了场景树,相机的信息,这都是出于通用性的考虑,但这个是否实用,就不好判断了。

Accessor&Json表述

这是glTF数据读取的机制,设计的很优雅,很值得我们学习。

通常,对一个二进制文件,我们都是按照规范格式逐个字节的解析,这样就有一个很大的风险,一步错了,后面的都会错。因为这太容易出错了,万一版本升级,多加了几个字节,就会导致整个文件无法解析,我们增加了超级纠错的机制。对每一块数据前面加一个长度,或者校验码,检测这块数据是否完整,每块读完后,根据这块的长度跳到下一个二进制块重新开始(而不是循序渐进),这样每一块坏了不会影响下一块。问题解决了,但并不实用,还仅仅是从技术层面的处理,所以我们需要增加规范,从设计上来解决这个问题,增加一个表述信息,根据accessor的规范来读取二进制流。(个人猜测protocol buffer也是这样的设计思路)

当然,如果实现这种读取方式,我们就需要一个“目录”页,glTF提供了json形式的header,这个header是json形式,也可以是xml格式,好处是灵活,兼容性强。相比xml,json是浏览器内部封装成对象,效率高,缺点查询不方便。

产品化

万事开头难,何况是创造一个新的东西,而且,当这个新的东西落地后,一切才刚刚开始。你需要完善的文档和配套工具,需要和相关的厂商(数据&硬件)合作,是否开源,许可协议等,一堆事情要做,而且要做好。如果只是草草了事,只是看起来漂亮,还是无法得到别人的认可,纯属自娱自乐的行为,那就大为失色了。

不妨低调的提供一个不是规范的规范,有了足够的项目实践和完善,踏踏实实的用起来,得到了用户的认可,文档工作也做细致了,标准化规范化自然也就提上了议程。好的技术,好的产品,好的标准,一切都是环环相扣的,所以,做好每一步,顺其自然,就像那首歌一样,Que sera sera, Whatever will be will be。

不知道有多少看到这,不容易啊,推荐有缘人看看这部推荐的电影,一部关于自闭症的动画片,我很喜欢~

时间: 2024-10-17 00:47:39

Cesium原理篇:glTF的相关文章

Cesium原理篇:7最长的一帧之Entity(下)

上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSourceDisplay.Visualizer中.本篇则从Visualizer开始,介绍数据的处理,并最终实现渲染的过程. CesiumWidget.prototype.render = function() { if (this._canRender) { this._scene.initializ

Cesium原理篇:3最长的一帧之地形(2:高度图)

       这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面.        此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维护地形切片的相关逻辑:接着,在requestTileGeometry中,TileTerrain会请求对应该切片的地形数据.如果读者对这部分有疑问的话,可以阅读<Cesium原理篇:1最长的一帧之渲染调度>:最后,如果你是采用的高度图的地形服务,地形数据对应的是HeightmapTerrainDat

Cesium原理篇:3最长的一帧之地形(3:STK)

有了之前高度图的基础,再介绍STK的地形相对轻松一些.STK的地形是TIN三角网的,基于特征值,坦白说,相比STK而言,高度图属于淘汰技术,但高度图对数据的要求相对简单,而且支持实时构建网格,STK具有诸多好处,但确实有一个不足,计算量比较大,所以必须预先生成.当然,Cesium也提供了一个Online的免费服务,不过因为是国外服务器,所以性能和不稳定因素都不小.好的东西自然得来不易,所以不同的层次,根据具体的情况选择不同的方案,技术并不是唯一决定因素,甚至不是主要因素. CesiumTerra

Cesium原理篇:3D Tiles(1)渲染调度【转】

Cesium在2016年3月份左右推出3D Tiles数据规范,在glTF基础上提供了LOD能力,定位就是Web环境下海量三维模型数据.虽然目前3D Tiles还是Beta阶段,有不少硬伤,但3D Tiles数据规范于2016年9月30日开始了OGC标准化进程,积极成分还是很大. 之前的glTF时分享了个人对二进制格式的一些想法和谨慎的态度.3D Tiles简单说就是具备LOD能力的glTF.有了数据首先是提供API可以渲染,保证用起来,下一步就要了解该数据规范的具体特点,比如倾斜,矢量,点云,

Cesium原理篇:6 Renderer模块(2: Texture)

Texture也是WebGL中重要的概念,使用起来也很简单.但有句话叫大道至简,如果真的想要用好纹理,里面的水其实也是很深的.下面我们来一探究竟. 下面是WebGL中创建一个纹理的最简过程: var canvas = document.getElementById("canvas"); var gl = canvas.getContext("webgl"); // 创建纹理句柄 var texture = gl.createTexture(); // 填充纹理内容

Cesium原理篇:6 Render模块(3: Shader)

在介绍Renderer的第一篇,我就提到WebGL1.0对应的是OpenGL ES2.0,也就是可编程渲染管线.之所以单独强调这一点,算是为本篇埋下一个伏笔.通过前两篇,我们介绍了VBO和Texture两个比较核心的WebGL概念.假设生产一辆汽车,VBO就相当于这个车的骨架,纹理相当这个车漆,但有了骨架和车漆还不够,还需要一台机器人来加工,最终才能成产出这辆汽车.而Shader模块就是负责这个生产的过程,加工参数(VBO,Texture),执行渲染任务. 这里假设大家对Shader有一个基本的

Cesium原理篇:6 Render模块(4: FBO)

Cesium不仅仅提供了FBO,也就是Framebuffer类,而且整个渲染过程都是在FBO中进行的.FBO,中文就是帧缓冲区,通常都属于高级用法,但其实,如果你了解了它的基本原理后,用起来还是很简单的,关键在于理解.比如你盖楼,地基没打好,盖第一层楼,还可以,盖第二层楼,有点挫了,盖第三层楼,塌了.你会认为第三层楼(FBO)太难了,其实根本原因还是出在地基上. 窗口系统所管理的帧缓存有自己的缓存对象(颜色,深度和模板),它们诞生于窗口创建前,而我们自己创建的帧缓冲,这些缓存对象则需要自己来手动

Cesium原理篇:3最长的一帧之地形(4:重采样)

       地形部分的原理介绍的差不多了,但之前还有一个刻意忽略的地方,就是地形的重采样.通俗的讲,如果当前Tile没有地形数据的话,则会从他父类的地形数据中取它所对应的四分之一的地形数据.打个比方,当我们快速缩放影像的时候,下一级的影像还没来得及更新,所以会暂时把当前Level的影像数据放大显示, 一旦对应的影像数据下载到当前客户端后再更新成精细的数据.Cesium中对地形也采用了这样的思路.下面我们具体介绍其中的详细内容.        上图是一个大概流程,在创建Tile的时候(prepa

cesium原理篇(二)--网格划分【转】

转自:http://www.cnblogs.com/fuckgiser/p/5772077.html 上一篇我们从宏观上介绍了Cesium的渲染过程,本章延续上一章的内容,详细介绍一下Cesium网格划分的一些细节,包括如下几个方面: 流程 Tile四叉树的构建 LOD 流程 首先,通过上篇的类关系描述,我们可以看到,整个调度主要是update和endFrame两个函数中,前者分工,后者干活. 另外,QuadtreePrimitive类只要来维护整个地球的四叉树,而每一个Tile对应一个Quad