WebGL Insights - WebGL 实现 - 图1

《WebGL Insights》是讲解 WebGL 实现与实践的书籍,由主编 Patrick Cozzi 在 2015 年完成。2015 年恰巧是 WebGL 2.0 发布的一年,所以此书主要以 WebGL 1.0 为主,但是会涉及部分 WebGL 2.0 的内容。

主编 Ptrick Cozzi 的主要方向是三维地理信息系统(3D GIS),同时也是 Khronos Group 成员,glTF 主要设计者之一,Web 3D GIS 开源库 CesiumJS 的发起者。他联系其他早期 WebGL 使用者分享 WebGL 实施或使用中的经验,总结为此书,所以此书对 WebGL 开发者来说非常用意义,在有了一定的 WebGL 开发经验后,可以从原理入手,了解更多 WebGL 实践过程中的的优化经验。

此书主要由七部分组成

  1. WebGL 实现

  2. 使用

  3. 移动端

  4. 引擎设计

  5. 渲染

  6. 可视化

  7. 交互

此文将概述第一部分 - WebGL 实现 中的主要内容。

第一部分主要有三篇分享,分别来自 ANGLE,Mozilla,Chrome,分享 WebGL 在实现过程中的经验。为什么选择这三个组织进行分享呢?这个其实是非常有道理的。

首先 ANGLE 可以说是目前 OpenGL ES 2.0 (WebGL 1.0 实现的就是此接口)的最好封装层,可以支持 Direct3D 9,Direct3D 11,是 Chrome,Firefox,Opera 在 Windows,OS X 平台上实现 WebGL 的主要依赖库,Chrome 和 Firefox 在 Linux 平台上也是使用的 ANGLE,所以可以说 ANGLE 是 WebGL 的基石,为统一多平台的不同图形接口提供巨大帮助。

其次 Chrome 和 Firefox 作为主流浏览器,对应的 Chrome 小组以及 Mozilla 社区也有分享 WebGL 实现的资格。

ANGLE

浏览器收集到 WebGL 的指令后,将转换为 Native 的图形接口,这一点在移动端比较容易实现,因为移动端使用的是 OpenGL ES 接口,与 WebGL 非常相似。但是在桌面端,情况就比较复杂,Linux 和 OS X 有对 OpenGL ES 的支持,但是 Windows 使用的是 DirectX,成为实现 WebGL 的主要阻力。在 2013 年时 ANGLE 实现了对 DirectX 11 的支持,同时支持 DirectX 9,自此以后便有了面向 OpenGL ES 的跨平台的统一图形接口,ANGLE 作为封装层,将 OpenGL ES 接口转换为平台本身的图形接口。这一年 WebGL 1.0 问世,WebGL 的实现架构简略的概括为下图,绿色部分就是 ANGLE 的主要任务。

WebGL Insights - WebGL 实现 - 图2

ANGLE 是一个转换器而不是模拟器,就是说他更像是提供了 OpenGL ES 的实现,显卡本身是支持任何通用的光栅化图形管线的,ANGLE 只是为显卡提供了这部分缺失的驱动,不会因为这个损失性能。从宏观上看,所有的光栅化图形 API 包含两种操作:设置状态和绘制。设置状态主要包括剔除模式,Attribute 布局,设置 Shader 等,设置好所有状态后将执行绘制,绘制时将收集所有的状态进行光栅化,绘制指令就是是 glDrawArraysglDrawElements 之类。所以 ANGLE 只是一个转换器转换指令,并不是一个模拟器模拟渲染管线。

OpenGL ES 和 DirectX 都是底层的的渲染 API,所以 WebGL 可以成功转换。之所以不直接从 WebGL 转换到 DirectX 是出于一致性考虑,因为这样不能保证不同底层接口结果是相同的,所以才需要 ANGLE 这个中间层负责转换。

ANGLE 在自己负责的这一部分功能内提出了 WebGL App 需要注意的点:

  • WebGL 1.0

    • 避免使用 LINE_LOOP 和 TRIANGLE_FAN,因为 LINE_LOOP 在 DirectX 中不支持,TRIANGLE_FAN 在 DirectX 11 中不支持,这样的情况就会让 ANGLE 层为了模拟这个接口付出更多 Overhead。

    • 创建新的 Texture 而不是重设原 Texture 的尺寸,由于数据同步问题,重置材质比新建材质产生更多 Overhead。

    • 不要在有 Scissor 或 Mask 时执行 Clear,因为 DirectX 和 OpenGL 在 Clear 时是否应用这些状态时不一致的。

    • 用 MeshLine 绘制粗线,因为需求和性能要求不同时,粗线的绘制规则应该放在应用层。

    • 避免使用 Uint8Array/Uint16Array 类型的三通道 Vertex Buffer(如 [Uint8Array, Uint8Array, Uint8Array]),使用四通道代替,32 位情况不用在意。

    • 避免使用 Uint8Array 数据作为 Index Buffer,考虑 DirectX 的原生支持情况。

    • 避免在 16 位的 Index Buffer 使用 0xFFFF,因为这个值在 OpenGL ES 3.0 中用于表示终止点,应使用 32 位的数据类型代替。

    • 正确的使用 Buffer 状态标志,如 STATIC_DRAW。

    • 一定要声明 Fragment Shader 的浮点精确度(一般不声明会报错)。

    • 不要使用 Rendering Feedback Loops,就是不要对 Texture 或 Renderbuffer 同时进行读和写(WebGL 其实不支持同时读写)。

  • WebGL 2.0

    • 在使用扩展时一定要有退路,就是要有扩差不支持的解决办法。

    • 尽可能使用 Immutable Textures,这个和 Texture 的创建有关,避免 ANGLE 产生过多 Overhead。

    • 使用 RED Texture 代替 LUMINANCE,考虑 DirectX 的支持情况,当需要单通道数据时可以使用
      EXT_texture_rg 扩展使用 RED 格式。

    • 避免使用 Int 型 CubeMap Texture,出于 DirectX 的支持性考虑。

    • 避免使用 UBO Offset,出于 DirectX 的支持性考虑。

    • 在 2D Texture Array 中不要使用 Shadow Lookups,因为 DirectX 11 不支持。

最后讲解了如何在 Chrome 中调试 ANGLE,感兴趣的可以看看。

Mozilla

Mozilla 主要分享了 Firefox 对 WebGL 的实现,同时分析了值得考虑的 Overhead 产生自哪些操作。

下面是一张图描述的 WebGL 方法调用的实现流程。

WebGL Insights - WebGL 实现 - 图3

DOM Bindings

DOM Binding 作为 WebGL 方法执行的第一步,做的事情就是将 WebGL 方法和参数翻译成对应的 C++ 代码,并调用集成 WebGL 对应的 C++ 代码。

举个例子,当调用以下代码时

  1. gl.uniform4f(location, x, y, z, w);

根据 WebGL IDL 得知,location 应该是 WebGLUniformLocation 对象,xyzw 应该是数字,所以 WebGL 集成必须验证参数的类型,在错误时抛出异常。

Firefox 使用 Python 脚本将 WebGL 调用转换为 C++ 代码,这个操作是非常频繁的,需要在每一帧的每一个 WebGL 指令上执行。其次是单次转换的耗时随着参数的个数线性增长。所以在这一步 Overhead 的主要影响因素就是 WebGL 的指令数以及每个指令的参数个数。

当翻译完成后,调用的是为集成 WebGL 实现的 C++ 接口,所以 DOM Binding 可以由下图表示。
WebGL Insights - WebGL 实现 - 图4

WebGL Method Implementations and State Machine

为什么浏览器要对 WebGL 的方法进行一层手动的 C++ 封装?而不是直接调用对应的 OpenGL 接口?WebGL 的集成方法主要做有效性验证,错误和异常抛出。虽然 DOM Binding 阶段将参数转换成期望的格式,但是参数是否有意义,并不知道在连续的上下文中其他状态的有效性,不能完全依赖 OpenGL 进行异常捕捉,所以浏览器需要维持一套状态,以及查询硬件的参数,去验证命令的有效性,抛出有意义的异常。为了做到这个,在对纹理进行缓存及格式转换将造成大量 Overhead,感兴趣的可以看看原文。

Shader Compilation

WebGL 集成必须包含自己的着色器编译器,首先 GLSL 和 GLSL ES 的标准不太一样,GLSL ES 1.0 和又是 GLSL ES 中的一个比较严格的子集,为了考虑支持性 WebGL 1.0 GLSL 使用的就是 GLSL ES 1.0,所以需要自己的转换器去验证着色器代码,Firefox 使用的是 ANGLE 的着色器编译器。

所以着色器代码需要经历以下转换
WebGL Insights - WebGL 实现 - 图5
当在 Windows 平台上时,需要在最后多一次转换(DirectX Shader Complier)。

验证

在执行 Draw Call 时,需要对 AttributeArrays 进行验证,当存在 IndexBuffer 时需要验证 Index 不会超限,NULL 或者不全的 Texture 需要用 1x1 黑色 Texture 代替,这里的细节可以查看原文。

Chrome

Chrome 主要分享了使用 GPU Try Servers 实现 WebGL 的持续集成测试,确保 WebGL 的可靠性,这里不会细致探讨,感兴趣的可以查看原文。

最后,WebGL 其实是一种权衡的技术,它既要能够给 Web 端带来高效的事实图形能力,又要考虑多平台的兼容性,性能。所以最终需要在兼容性和特性,性能之间进行平衡,在这个平衡中往前推进。所以使用 WebGL 时就应该考虑这些因素,不能寄希望于同时拥有跨平台的兼容性优势,又希望拥有 Native 的特性和高性能,WebGL APP 需要实现考虑好自己的需求是什么,在权衡利弊后选择合适的技术方案才是最好的。