关键字: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。

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

Cesium原理篇:glTF的更多相关文章

  1. Cesium原理篇:5最长的一帧之影像

    如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了.之前的几个系列,我们全面的介绍了Cesium的地形内容,详见: Cesium原理篇:1最长的一帧之渲染调度 Cesium原 ...

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

           这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面.        此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维 ...

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

    上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSo ...

  4. Cesium原理篇:3D Tiles(2)数据结构

    上一节介绍3D Tiles渲染调度的时候,我们提到目前Cesium支持的Cesium3DTileContent目前支持如下类型: Batched3DModel3DTileContent Instanc ...

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

    有了之前高度图的基础,再介绍STK的地形相对轻松一些.STK的地形是TIN三角网的,基于特征值,坦白说,相比STK而言,高度图属于淘汰技术,但高度图对数据的要求相对简单,而且支持实时构建网格,STK具 ...

  6. Cesium原理篇:6 Render模块(6: Instance实例化)

    最近研究Cesium的实例化,尽管该技术需要在WebGL2.0,也就是OpenGL ES3.0才支持.调试源码的时候眼前一亮,发现VAO和glDrawBuffers都不是WebGL1.0的标准函数,都 ...

  7. Cesium原理篇:3D Tiles(1)渲染调度

    Cesium在2016年3月份左右推出3D Tiles数据规范,在glTF基础上提供了LOD能力,定位就是Web环境下海量三维模型数据.虽然目前3D Tiles还是Beta阶段,有不少硬伤,但3D T ...

  8. Cesium原理篇:3D Tiles(3)个人总结

    个人结论:目前,在演示层面,3D Tiles问题不大,但项目应用上就不够成熟了,所以问问自己,你是想吃瓜呢还是想吃螃蟹? 好的方面 数据规范 我非常喜欢glTF的整体设计,概括有四点:第一,数据块(B ...

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

    Cesium在2016年3月份左右推出3D Tiles数据规范,在glTF基础上提供了LOD能力,定位就是Web环境下海量三维模型数据.虽然目前3D Tiles还是Beta阶段,有不少硬伤,但3D T ...

随机推荐

  1. js 日期控件laydate使用

    官网  http://sentsin.com/layui/laydate/ 1. 下载官网上的压缩包,解压后只需要复制laydate 文件夹到你的项目中; 2. 在页面引入  <script t ...

  2. ZOJ 3933 Team Formation

    费用流裸题......比赛的时候少写了一句话....导致增加了很多无用的边一直在TLE #include<cstdio> #include<cstring> #include& ...

  3. ZOJ 3780 Paint the Grid Again

    拓扑排序.2014浙江省赛题. 先看行: 如果这行没有黑色,那么这个行操作肯定不操作. 如果这行全是黑色,那么看每一列,如果列上有白色,那么这一列连一条边到这一行,代表这一列画完才画那一行 如果不全是 ...

  4. GCJ 2008 APAC local onsites C Millionaire

    时间复杂度很大.dp[i][j]表示第i轮 j这种状态的概率. #include<cstdio> #include<cstring> #include<cmath> ...

  5. C#基础之操作字符串的方法

    C#基础之操作字符串的方法 C#中封装的对字符串操作的方法很多,下面将常见的几种方法进行总结: 首先定义一个字符串str 1.str.ToCharArray(),将字符串转换成字符数组 2.str.S ...

  6. SD卡初始化以及命令详解

    SD卡是嵌入式设备中很常用的一种存储设备,体积小,容量大,通讯简单,电路简单所以受到很多设备厂商的欢迎,主要用来记录设备运行过程中的各种信息,以及程序的各种配置信息,很是方便,有这样几点是需要知道的 ...

  7. 【转】Linux目录下/dev/shm的理解和使用

    一般来说,现场部署  都要根据内存的大小来设定/dev/shm的大小,大部分使用的是默认的值! Linux目录下/dev/shm的理解和使用 [日期:2014-05-16] 来源:Linux社区  作 ...

  8. windows Vista-Server 2016系列激活脚本.cmd

    @ECHO OFFTITLE Windows 全版本系统激活ECHO 检测 操作系统版本...SET RQR=REG QUERY "HKLM\SOFTWARE\Microsoft\Windo ...

  9. python实现二叉树

    初学python,需要实现一个决策树,首先实践一下利用python实现一个二叉树数据结构.建树的时候做了处理,保证建立的二叉树是平衡二叉树. # -*- coding: utf-8 -*- from ...

  10. windows批处理實例

    實例: 假設我們有個資料夾為d:\tmp和e:\tmp ,而我們只要將d:\tmp中有異動的檔案複製到e:\tmp下的話,用法如下 xcopy d:\tmp\. e:\tmp\ /D /S /Y 實例 ...