Draco几何压缩与Cesium中的应用

Draco压缩的glTF格式规范

如果primitive中的extension属性包含键KHRdracomesh_compression,那么Draco几何压缩就会启用。

Primitive中可以包含压缩和未压缩两个版本的几何数据,如果未压缩版本没有提供,需要在extensionRequired中声明KHR_draco_mesh_compression,如下:

"extensionsRequired" : [
    "KHR_draco_mesh_compression"
]

如果extensionRequired中已经声明了KHR_draco_mesh_compression,那么primitive中只能包含Draco压缩的数据(只是理论规范上的要求,Cesium的glTF也没有遵循这条规则)。如果Draco压缩版数据存在,那么extensionsUsed中需要声明KHR_draco_mesh_compression。 下面是一个包含Draco压缩拓展的样例glTF:

"mesh" : {
    "primitives" : [
        {
            "attributes" : {
                "POSITION" : 11,
                "NORMAL" : 12,
                "TEXCOORD_0" : 13,
                "WEIGHTS_0" : 14,
                "JOINTS_0" : 15
            },
            "indices" : 10,
            "mode" : 4,
            "extensions" : {
                "KHR_draco_mesh_compression" : {
                    "bufferView" : 5,
                    "attributes" : {
                        "POSITION" : 0,
                        "NORMAL" : 1,
                        "TEXCOORD_0" : 2,
                        "WEIGHTS_0" : 3,
                        "JOINTS_0" : 4
                    }
                }
            }
        }
    ]
}

"bufferViews" : [
    // ...
    // bufferView of Id 5
    {
        "buffer" : 10,
        "byteOffset" : 1024,
        "byteLength" : 10000
    }
    // ...
}

其中: bufferView指向包含压缩数据的buffer。压缩数据需要通过网格解压工具加压成网格(下文会提供几个解压工具)

attributes定义了解压后几何数据中的attribute和对应的unique id。比如上面样例中的POSITION,NORMAL,TEXCOORD0,WEIGHTS0和JOINTS_0。每个attribute在压缩的数据中有一个unique id。Draco解压工具通过这个unique id获取压缩数据中的attribute值。attributes拓展中定义的属性名必须是primitiveattribute属性名的子集。

accessors中对应primitiveattributesindices需要与解压后的数据吻合(但是Draco decode的时候似乎会忽略accessor中的bufferViewbyteOffset)。

primitive的mode只能是TRIANGLES或者TRIANGLE_STRIP

官方Github提供的加载Draco glTF的通用流程

  • 获取KHR_draco_mesh_compressionbufferView对应的数据,通过Draco decoder解压成一个Draco Geometry对象。
  • 然后处理primitive中的attributesindices属性。当加载accessor时,我们需要忽略accessor中原有的bufferViewbyteOffset,去之前解压的Draco Geometry对象中获取attributesindices。然后使用解压的数据填入accessors或者直接用Draco geometry渲染图形。 参考文献:Draco glTF

Cesium加载Draco压缩的glTF逻辑

前提

Cesium本身没有解码(decode)Draco glTF的代码,Cesium在解压Draco数据时使用的是第三方库draco_decoder.jsSource/ThirdParty/Workers/draco_decoder.js)。该第三方库是Google Draco官方的提供的JavaScript Decoder API。代码是压缩过的,源码是通过emscripten由C++转成JavaScript的(参考API)。
Draco_decoder.js需要配合一个WebAssembly二进制文件(wasm)和wasm的wrapper才可以使用。

0. Cesium中涉及到处理Draco压缩的代码文件

Cesium端代码:

  • Source\Scene\Model.js 模型加载和处理
  • Source\Scene\DracoLoader.js Draco加载器
  • Source\Workers\decodeDraco.js Cesium的Draco解压API

第三方库:

  • Source/ThirdParty/Workers/draco_wasm_wrapper.js
  • Source/ThirdParty/draco_decoder.wasm
  • Source/ThirdParty/Workers/draco_decoder.js

1. Model.js中定义DecodeLoader进行decode操作

Model.js中定义了一个DracoLoader对象,使用DracoLoader.hasExtension(this)判断是否包含Draco拓展。

使用DracoLoader.parse(this, context)将压缩的数据存入model._loadResources.primitivesToDecode队列中。 使用DracoLoader.decode(this, context)进行decode操作。

2. DracoLoader.js中的decode方法

Cesium为了保证交互流畅,使用Web Workder进行解压操作。decode方法调用了名为scheduleDecodingTask的函数,负责将model放入Web Worker中进行解压,最后返回一个promise

Promise执行完成返回的结果result就是解压后的数据,包含attributeDataindexArray(也就是attribute和indices)。解压的输出存入model._decodedData中,之后的逻辑与Cesium普通模型的处理相同。

3. decodeDraco.js中的处理

decodeDraco.js是一个Worker脚本,其中调用了之前提到的第三方库draco_decoder.js,在该脚本中名为draco。

Draco_decoder.js的用法在Draco官方Github上有详细说明,以下列举一个代码片段:

// Create the Draco decoder.
const decoderModule = DracoDecoderModule();  
const buffer = new decoderModule.DecoderBuffer();  
buffer.Init(byteArray, byteArray.length);

// Create a buffer to hold the encoded data.
const decoder = new decoderModule.Decoder();  
const geometryType = decoder.GetEncodedGeometryType(buffer);

// Decode the encoded geometry.
let outputGeometry;  
let status;  
if (geometryType == decoderModule.TRIANGULAR_MESH) {  
  outputGeometry = new decoderModule.Mesh();
  status = decoder.DecodeBufferToMesh(buffer, outputGeometry);
} else {
  outputGeometry = new decoderModule.PointCloud();
  status = decoder.DecodeBufferToPointCloud(buffer, outputGeometry);
}

// You must explicitly delete objects created from the DracoDecoderModule
// or Decoder.
decoderModule.destroy(outputGeometry);  
decoderModule.destroy(decoder);  
decoderModule.destroy(buffer);  

Cesium中的做法和上述代码类似:

首先在initWorker函数中dracoDecoder = new draco.Decoder();定义了dracoDecoder对象。

decodeDracoPrimitive函数中var buffer = new draco.DecoderBuffer();定义了Draco的buffer;使用init()方法初始化Draco buffer。

通过dracoDecoder.GetEncodedGeometryType(buffer),获取几何体的类型,Cesium只支持三角网格类型。但是Draco还支持点云数据的压缩。

然后用var dracoGeometry = new draco.Mesh()。定义了Draco的Mesh对象。通过dracoDecoder.DecodeBufferToMesh(buffer, dracoGeometry);将压缩的buffer数据转成Draco网格。

此时已经完成了Draco解压,然后需要从解压出来的DracoGeometry中提取indexArrayattributeData。 Cesium使用decodeIndexArray函数创建indices数组,大致做法是通过dracoGeometry的方法获取面(faces)的数据,然后遍历每个面,用dracoDecoder.GetFaceFromMesh(dracoGeometry, i, faceIndices),获取每个面的节点索引。

Cesium使用decodeAttributeData函数提取attribute。每个attribute在Draco压缩数据中都有一个uniquie id。通过dracoDecoder.GetAttributeByUniqueId(dracoGeometry, unique_id);可以获取attribute的值。

最后返回第2步Promise返回的result。

客户端加载Draco glTF的思路

Draco本身的encoder和decoder是通过C++语言开发的。JavaScript decoder也是编译自C++的。所以Draco的Github自带C++ Decoder API。以下是C++ decoder的样例:

draco::DecoderBuffer buffer;  
buffer.Init(data.data(), data.size());

const draco::EncodedGeometryType geom_type =  
    draco::GetEncodedGeometryType(&buffer);
if (geom_type == draco::TRIANGULAR_MESH) {  
  unique_ptr<draco::Mesh> mesh = draco::DecodeMeshFromBuffer(&buffer);
} else if (geom_type == draco::POINT_CLOUD) {
  unique_ptr<draco::PointCloud> pc = draco::DecodePointCloudFromBuffer(&buffer);
}

可以看出,C++ decoder API和JavaScript decoder API的对象名和方法名基本相同,使用方法也大致相同。因此可以参考Cesium的decode方法(主要是脚本decodeDraco.js中的方法),根据解压的数据创建indices,并提取attribute

C++ decoder的代码参考Draco官方Github

本文所在的Git代码仓库克隆自Draco官方Github。

浙ICP备16041529号-1