WebGL-WebGL2迁移实践

6,227 阅读8分钟

WebGL2是2017年推出的Web 3D图形API,是对WebGL1的一次重大升级。在近几年的发展中,各家硬件设备及浏览器也逐渐完善了对WebGL2的支持。最近JSAPI在尝试实现一些新功能时发现需要使用到WebGL2的新特性,于是做了下迁移到WebGL2的调研和尝试,原来还挺简单的,快来一起迁移起来吧~

WebGL2

首先需要了解下WebGL2相对于第一代有什么变化,能给项目带来什么收益。

WebGL2的发展

盗图一张来说明下WebGL的发展过程:

WebGL版本演进

WebGL2是基于OpenGL ES 3.0实现的Web API,核心是WebGL2RenderingContext接口,是对WebGL1上下文对象的一次扩展,在100%向后兼容的基础上还提供了非常多的新特性。

目前各家浏览器的支持情况如下:

caniuse-webgl2

可以看到除了Safari(包括iOS Safari)、QQ浏览器、IE之外,其他浏览器基本都能支持了。在不能支持的浏览器上也可以做降级处理,采用WebGL1。

注: 除了WebGL之外,还有更加全新的Web3D图形API,那就是WebGPU,是浏览器基于现代图形API(Dx12、Vulkan、Metal)封装的应用接口,据说能实现更低的驱动开销,提供更好的计算性能。不过目前看来还没有得到广泛支持,而且与WebGL差别很大,不易兼容,所以暂不考虑WebGPU方案,感兴趣的同学可以关注下最新进展。

WebGL2的优势

1)无需扩展就能使用的重要特性

在使用WebGL1的过程中我们常用到一些扩展来支持一些高端功能,比如利用WEBGL_depth_texture深度纹理可以用来实现基于深度的雾化效果、阴影贴图等。

扩展应用起来比较麻烦,不仅要考虑各浏览器的兼容性,比如:

const ext = gl.getExtension('WEBGL_depth_texture') || gl.getExtension('MOZ_WEBGL_depth_texture') ||  gl.getExtension('WEBKIT_WEBGL_depth_texture');

而且提供的扩展方法也是在扩展对象上,方法名还有后缀,比如:

const ext = gl.getExtension('ANGLE_instanced_arrays');
ext.vertexAttribDivisorANGLE(location, 0);

不过很多扩展都已经成为了WebGL2的标准内容了,也就可以在上下文对象上直接调用原本扩展才能提供的方法了,而且有更多的参数支持。以下是JSAPI中有使用到或者计划使用的扩展功能:

功能 WebGL1扩展 WebGL2支持情况
深度纹理 WEBGL_depth_texture 支持
实例化渲染 ANGLE_instanced_arrays 支持
顶点数组对象 OES_vertex_array_object 支持
Int32类型索引 OES_element_index_uint 支持
显卡信息 WEBGL_debug_renderer_info 不支持
上下文丢失/恢复 WEBGL_lose_context 不支持

经整理WebGL2已纳入标准的扩展有:

extensions-in-webgl2

2)多绘制缓冲

多绘制缓冲,也叫做MRT(Multiple Render Targets),是促使我们迁移到WebGL2的动力之一。它可以支持在一个帧缓冲区上绑定多个颜色缓冲区,一次绘制可以在多个缓冲区中写入数据,可用于实现延迟渲染和泛光效果。运用这项技术后,光照及阴影的计算只需要在每个片元上执行一次,而且可以尽量和几何渲染过程解耦。不过也会带来一些问题,性能提升程度也待验证。

注: WebGL也可以使用WEBGL_draw_buffers来实现多个颜色缓冲区的绑定。

3)纹理功能增强

WebGL2支持了更多的纹理格式和压缩格式,太多了就不一一列举了。可以做什么呢?我目前能想到的一个是浮点帧缓冲,在颜色缓冲区中保存超过1.0的数值,以应用HDR技术调整光照效果。另一个是可以利用压缩纹理优化纹理缓存空间和上传耗时。

如果使用JPG或PNG图片格式,使用texImage2D上传图片数据到GPU时会进行解压,转为位图。位图格式占用空间较大,传输时间较长。而如果使用压缩格式,则可以不进行解压,同时节省空间和时间。WebGL1中通过扩展可以使用压缩纹理格式,但很多是要区分硬件条件的,S3TC基本上只是PC支持,PVTC只有iOS,而WebGL2中多种压缩格式都可以在任何环境下得到支持。

另外WebGL2还支持了纹理数组,多个相同大小的纹理切片可以共享一个纹理单元。纹理的访问也可以支持直接选取纹素。如何应用还待慢慢发掘。

4)Uniform缓冲对象

Uniform Buffer Object可以让我们像attribute变量一样,把uniform变量写入缓冲区,多次使用。一方面在uniform变量较多时,使用gl.bufferDatagl.bindBufferRange两次调用替代N次gl.uniformXXX,节省上传时间。另一方面,当多个着色器共用一些uniform变量时,可以一次写入,多处绑定即可。这种情况非常常见,比如matrix、center、zoom。

WebGL2的缺陷

  1. 浏览器兼容性:如上文所述,Safari、QQ浏览器均不支持WebGL2,且还是主流浏览器,所以必须做好二手准备,降级为WebGL1
  2. 可能会被下一代WebGPU替代:到时候再换不迟,毕竟看过下文后你会发现切换到WebGL2真的没什么成本

WebGL1迁移WebGL2

迁移本身成本很小,只是因为要做WebGL1兼容所以有些特殊处理,一步步来吧:

上下文兼容处理:优先获取webgl2上下文

这一步很简单:

// 优先使用WebGL2,若不支持则降级为WebGL(Safari/QQ浏览器)
let gl = canvas.getContext('webgl2', options);
if (gl) {
  gl.isWebGL2 = true;
} else {
  gl = canvas.getContext('webgl', options);
  gl.isWebGL2 = false;
}

扩展兼容处理:将扩展方法复制到上下文对象

对于一些WebGL2已经支持的扩展内容,在调用时做两种上下文的区分非常麻烦:

if (gl.isWebGL2) {
  gl.vertexAttribDivisor(location, 0);
} else {
  const ext = gl.getExtension('ANGLE_instanced_arrays');
  ext.vertexAttribDivisorANGLE(location, 0);
}

如果把扩展的方法直接赋加到上下文对象上,且与WebGL2使用相同的名字,那就可以使用同一套代码了。刚好扩展的属性及参数名都是有规律可循的,与WebGL2提供的方法相比都是多了一个后缀,具体改造方法如下,搬移至迁移WebGL1到WebGL2

function getAndApplyExtension(gl, name) {
  const ext = gl.getExtension(name);
  if (!ext) {
    return null;
  }
  const fnSuffix = name.split("_")[0];
  const enumSuffix = '_' + fnSuffix;
  for (const key in ext) {
    const value = ext[key];
    const isFunc = typeof (value) === 'function';
    const suffix = isFunc ? fnSuffix : enumSuffix;
    let name = key;
    // WEBGL_compressed_texture_s3tc
    // 和WEBGL_compressed_texture_pvrtc不是true
    if (key.endsWith(suffix)) {
      name = key.substring(0, key.length - suffix.length);
    }
    if (gl[name] !== undefined) {
      if (!isFunc && gl[name] !== value) {
        console.warn("conflict:", name, gl[name], value, key);
      }
    } else {
      if (isFunc) {
        gl[name] = function(origFn) {
          return function() {
            return origFn.apply(ext, arguments);
          };
        }(value);
      } else {
        gl[name] = value;
      }
    }
  }
  return ext;
}

if (!gl.isWebGL2) {
  getAndApplyExtension(gl, 'ANGLE_instanced_arrays');
  getAndApplyExtension(gl, 'WEBGL_depth_texture');
  getAndApplyExtension(gl, 'OES_vertex_array_object');
}

处理完之后要记得将以前调用扩展方法的代码都更新一下。

纹理操作适配

以下是我自己踩过的坑,主要涉及到纹理的操作,请大家一定注意:

1)texImage2D的语法

texImage2D在WebGL2中的使用语法不同,必须传入widthheightborder

// WebGL1:
void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView? pixels);
void gl.texImage2D(target, level, internalformat, format, type, ImageData? pixels);
void gl.texImage2D(target, level, internalformat, format, type, HTMLImageElement? pixels);
void gl.texImage2D(target, level, internalformat, format, type, HTMLCanvasElement? pixels);
void gl.texImage2D(target, level, internalformat, format, type, HTMLVideoElement? pixels);
void gl.texImage2D(target, level, internalformat, format, type, ImageBitmap? pixels);

// WebGL2:
void gl.texImage2D(target, level, internalformat, width, height, border, format, type, GLintptr offset);
void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLCanvasElement source);
void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLImageElement source); 
void gl.texImage2D(target, level, internalformat, width, height, border, format, type, HTMLVideoElement source); 
void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ImageBitmap source);
void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ImageData source);
void gl.texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView srcData, srcOffset);

2)深度纹理格式

如果你也使用了深度纹理,那么在调用texImage2D时还需要注意,在WebGL2中internalformat不可使用gl.DEPTH_COMPONENT(虽然有这个属性),在WebLG1中不可使用gl.DEPTH_COMPONENT16(虽然有这个属性),而且这些属性还是只读属性,所以我做了这样的兼容处理,这样上传时就能统一使用gl.DEPTH_COMPONENT24,暂时还没发现问题:

if (!gl.isWebGL2) {
  gl.DEPTH_COMPONENT24 = gl.DEPTH_COMPONENT;
}

另外需要注意的是,在WebGL1internalformatformat都是相同的,但是在WebGL2中是有对应关系的,比如这里不管internalformatgl.DEPTH_COMPONENT16还是gl.DEPTH_COMPONENT24gl.DEPTH_COMPONENT32F,对应的format都是gl.DEPTH_COMPONENT

3)深度纹理过滤

这真的是这次迁移过程中遇到的最大的坑。当我切换到WebGL2之后,从深度纹理中读取到的数据都是0,其他纹理没问题,深度测试也没问题,最后定位到问题出在纹理过滤的设置上。

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, options.minFilter || gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, options.magFilter || gl.LINEAR);

如上,之前都是采用线性过滤,WebGL1没有问题,但在WebGL2里就不好使了(也不知道为啥,求指教)。在mac上还好,还有个警告,windows上连警告都没有🙄️️。这里需要设置为gl.NEAREST才能正常使用。

着色器无需升级

总的来说,迁移成本还是相当低的,本来以为需要将着色器代码按GLSL 3.0的语法重写一遍,实际上是可以兼容旧着色器代码的,而如果要使用新语法,则只需要在着色器第一行注明:

#version 300 es

需要注意不能有空格或者空行。GLSL 3.0具体有哪些改变也可以参考迁移WebGL1到WebGL2