首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

加载 PLY 文件

到目前为止,我们的例程中都还是通过手写顶点坐标的方式构建几何体的。简单的立方体和三角形还好,但是复杂的模型再这么干就说不过去了。 为了方便存储、传递、复用模型,人们开发出了很多种格式的模型文件。对于一些主流的格式提供导入导出支持,是一个图形引擎的基本素养。

PLY 文件格式是一种早期的三维模型格式之一,全称为 Polygon File Format 或者 Stanford Triangle Format。这个模型格式十分简洁,而且具有很强的扩展性。在计算机图形、三维建模等诸多领域都有广泛的应用。 最早是Stanford Computer Graphics LabratoryGreg Turk 开发的。 著名的斯坦福兔子以及数字米开朗基罗的大卫雕像都用 PLY 格式存储。

1. PLY 文件格式

正如它的名字一样,PLY 文件以多边形集合的形式描述图形对象。PLY 将一个对象描述为顶点、面及其他元素的集合,同时还可以给这些元素上附加上各种属性,比如颜色、法向量、纹理坐标等。 一个 PLY 文件仅描述一个对象,并非通用的场景描述语言,或者是某种万能的建模格式。它不包含变换矩阵、层次的建模结构、对象的子部件等特性。

它有便于人类阅读的 ASCII 文本,以及便于机器存储的二进制两种形式。这两种形式都有一个文本形式的文件头,用于描述模型的一些基本信息。比如有多少个元素集合,是 ASCII 还是二进制文件, 元素的数据类型等等。文件头之后,会按照文件头中描述的顺序,依次列出每种元素类型的元素列表。它们都是一堆数值,所以有 ASCII 文本和二进制两种形式。

下面是一个完整的 ASCII 文本形式的 PLY 描述,其中 {} 中的内容只是注释说明并非文件的一部分。文件头以 'ply' 开始 'end_header' 结束。关键字 'format' 描述了文件的格式, 'ascii' 表示 ASCII 格式,'binary_little_endian' 为小端存储,'binary_big_endian' 为大端存储。接下来是以关键字 'element' 和 'property' 每种元素的描述。

        ply
        format ascii 1.0                         { 格式类型(ASCII/二进制)及版本号              }
        comment made by anonymous                { 以关键字comment开头的注释                     }
        comment this file is a cube
        element vertex 8                         { 有 8 个"vertex"元素                           }
        property float32 x                       { 每个元素都有 x,y,z 三个属性                   }
        property float32 y                       
        property float32 z                       
        element face 6                           { 有6个"face"元素                               }
        property list uint8 int32 vertex_index   { 每个元素都是整数列表,属性名为 "vertex_index" }
        end_header                               { 标记文件头结束                                }
        0 0 0                                    { 顶点列表开始                                  }
        0 0 1
        0 1 1                                    { 一般都是一个元素占一行 }
        0 1 0
        1 0 0
        1 0 1
        1 1 1
        1 1 0
        4 0 1 2 3                                { 面列表开始                                    }
        4 7 6 5 4
        4 0 4 5 1
        4 1 5 6 2
        4 2 6 7 3
        4 3 7 4 0

PLY 通过如下的描述格式来定义每个元素列表的。上面的立方体中定义了两个 element 列表,有 8 个顶点(vertex) 和 6 个面 (face)。 "element" 行之后紧跟着列出了它的属性(property),定义了属性的数据类型,也指定了每个元素中属性的排列顺序。 例如这里每个 vertex 都有三个单精度浮点(float32)属性,x,y,z 用来描述顶点坐标。每个 face 只有一个列表(list)属性,描述了各个面用到的顶点索引。

        element <元素名称> <文件中该元素的数量>
        property <数据类型> <属性名称1>
        property <数据类型> <属性名称2>
        property <数据类型> <属性名称3>
        ...

属性的数据类型分为三类:标量(scalar)、字符串(string)和列表(list)。其中标量和字符串很好理解,直接对照着 C 语言中的数据类型就可以了。 列表稍微特殊一点,它有两个数据类型,第一个用来描述列表的长度,第二个描述列表中各个成员的数据类型。

        property list <数据类型> <数值类型> <属性名称>

所以后面顶点列表中每一行都有三个数字,分别是顶点的 xyz 坐标。面列表中每一行有 5 个数字,第一个数字表示一个面有 4 个元素,后面是这个面所用到的顶点索引。 二进制文件也很好理解,就是按照 element 和 property 中关于数据类型的描述,依次保存这些数据即可。

2. 解析 PLY 文件

我们提供了 PLYLoader 通过它的成员函数 parse() 来解析 PLY 文件。 成员函数 pare() 支持 ASCII 或者二进制两种形式的格式,它的输入是一个 string 类型的文本或者 ArrayBuffer 类型的二进制数据。输出是关于 element 的字典。 bun_zipper.ply 是那只著名的斯坦福兔子。 成功通过例程加载之后, 如右图所示,从调试界面上可以看到这只兔子的模型一共有 35947 个顶点,69451 个面。

PLYLoader 的整体逻辑十分简单,对照上面的 PLY 文件格式的解析,不难理解,这里不再展开。我们来重点看一下在浏览器中如何加载一个本地文件。 如下图所示,我们给例程中增加了一个新的标签 <input>。它的默认样式就是图中左侧蓝色高亮的一个 "Choose File" 按钮和 "No file chosen" 的文本。

点击它就会出现一个对话框让我们选择要加载的文件,如下图所示。值得关注的是对话框右下角的下拉框,我们可以通过给 input 标签添加 accept 属性来过滤文件的后缀名。

在对话框中选中文件之后,就会触发一个 change 的事件。我们可以通过 document.getElementById 获取 input 标签对象,并通过 addEventListener 接口注册 change 事件的回调函数。 那么浏览器就会检测到 change 事件之后调用这个函数,完成 PLY 文件的读取和解析操作。

        let fileInput = document.getElementById('fileInput');
        fileInput.addEventListener('change', function() { /* 完成加载和解析 */ });

为了方便后续可能加载其它格式的模型文件,我们专门设计了一个中间层 Loader 用于根据文件后缀名选择不同的解析器。代码逻辑也比较简单,这里不再展开。我们只关注入口函数 load。

        /**
         * 加载一个本地文件
         * @param {File} file 通过 input 标签传入的文件对象
         * @param {function} onSuccess 成功加载后的回调函数 
         * @param {function} onError   出现异常时的回调函数
         * @param {function} onProgress 查看加载进度的回调函数  
         */
        load(file, onSuccess, onError, onProgress)

如果模型文件很大,那么加载和解析会比较耗时,我们不能让程序逻辑阻塞在那里,这样会让浏览器显得没有响应。所以我们通过 async 和 await 的关键字将 Loader 设计成异步执行的方式。 入口函数 load 有四个参数,其中第一个参数就是要加载的文件,剩下的是三个回调函数。如下面的注释所示。

load 函数会通过浏览器的 FileReader 接口加载文件,并调用 PLYLoader 完成解析。 如果一起顺利,就会将解析后的 Object 对象作为参数调用 onSuccess 函数。我们可以在这个函数里完成图形引擎的更新。 如果出现了异常,就会调用 onError 函数。当文件很大时,FileReader 接口会定期的产生一个 progress 的事件,用户可以注册该事件的回调函数,来实现类似进度条的功能。 Loader 的实现将这一事件透传调用到 onProgress 函数。

3. 渲染斯坦福兔子

函数 load 成功加载了 PLY 文件之后,我们会得到一个 object 类型的对象,其中的各个字段对应着原 PLY 文件中的各个 element 列表。 接下来,我们需要将它转换成 Object3D 类型的对象,送到 WebGL 中完成渲染。 根据 PLY 文件的格式,一个物体实际上就是由点和面两种元素构成的。所以我们在类 Object3D 中增加了函数 setVertexAttribute 用于构建顶点的各个属性, 和 setFaceIndices 用于描述构成各个面的顶点索引。

需要注意的是,在 WebGL2 中面只能通过三角形的方式渲染。如教程所述, 渲染平面时只支持 TRIANGLES, TRIANGLE_STRIP, TRIANGLE_FAN 三种参数。所以四边及以上的多边形需要做三角分割后, 才能送到 WebGL2 中渲染。四边形可以直接选择一个对角线完成分割,更多边的话算法就比较复杂,所以函数 setFaceIndices 只支持三角形或四边形的面元描述。

例程中,如果一切顺利, 把 bun_zipper.ply 加载进来之后,就会看到斯坦福的那只小兔子在旋转。 由于该文件中没有关于颜色的描述,所以我们写了一个着色器, 通过变量 uUseUniformColor 和 uPixelColor 指定一个默认颜色。

4. 完




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1