关于前端:译如何使用-WebGL-进行实时视频处理

35次阅读

共计 4538 个字符,预计需要花费 12 分钟才能阅读完成。

  • 原文地址:Realtime video processing with WebGL
  • 原文作者:Lea Rosema

这是我最近在 CodePen 上制作的 WebGL 演示案例。它能够捕捉网络摄像头的数据(或在无法访问网络摄像头时,从 placekitten 获取备用图像),并将其实时转换为 ASCII 图像艺术。

为了取得更多的复旧性,我应用了 90 年代 DOS PC 中常见的 8×8 像素光栅字体(您可能会在某些 BIOS 中看到这种字体)。

要将图像内容映射到特定字符,我通过应用亮度图抉择最佳匹配。我计算每个 4×4 正方形的像素。在画板内向下滚动以查看亮度图:

我还为这些字体创立了一个编辑器:https://terabaud.github.io/pi…

若干 WebGL 基础知识

我将介绍 WebGL 的一些基础知识,但这里仅波及局部问题。获取无关具体领导,建议您拜访 https://webglfundamentals.org

对于 WebGL,一个常见误会是把它当作浏览器中的 3D 引擎。只管 WebGL 技术能使咱们在浏览器中提供 GPU 减速的 3D 内容,但 WebGL 自身不是 3D 引擎。在 WebGL 之上,有专门用于 GPU 减速的 2D 或 3D 内容的图形库(例如用于 2D 的 Pixi,用于 3D 的 ThreeJS)。

WebGL 自身是很根底的绘图规范库,并且是一个以 GPU 减速的形式,将点、线和三角形绘制到 html <canvas> 元素上的库。

能够通过 getContext(相似于 2D canvas API)检索 WebGL 渲染上下文:

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

一个 WebGL 程序蕴含多个着色器组件,着色器是运行在 GPU 上的代码,它们不是用 JavaScript 编写的,而是具备本人的语言,称为 GLSL(GL 着色器语言)。

GLSL 疾速概览

  • 相似 C 语言,着色器程序蕴含 void main()
  • 变量申明也像在 C 语言中一样
  • 原始数据类型:int, float, double
  • 向量:vec2, vec3, vec4, …
  • 矩阵:mat2, mat3, mat4, …
  • 拜访纹理数据的类型:sampler2D
  • 内置向量、矩阵运算
  • 大量内置性能, 例如,求取向量的长度 (length(v))

着色器的类型

WebGL 程序中有两种类型的着色器。

  • 顶点着色器计算地位。
  • 片段着色器解决栅格化。

如果您的 WebGL 程序想要在屏幕上绘制一个三角形,它会把三角形的 3 个坐标传递给顶点着色器。而后,片段着色器的工作是用像素填充该三角形,这种逐像素处理过程十分快,因为它是针对 GPU 上的每个像素并行运行解决的。

在我的演示案例中,我应用 4 个矢量坐标来笼罩适宜整个屏幕的矩形,所有工作都在片段着色器中实现。

顶点着色器

顾名思义,顶点着色器存在于顶点。它从 JavaScript 代码提供的缓冲区中获取一堆数据,并依据这些数据计算在画布中的相应地位。

以下代码段将数据从缓冲区拉入一个 attribute 变量,并将其传递给该 gl_Position 变量:

attribute vec3 position;

void main() {gl_Position = vec4(position, 1.0);
}

片段着色器

precision highp float;

void main() {
  vec2 p = gl_FragCoord.xy;
  gl_FragColor = vec4(1.0, .5 + .5 * sin(p.y), .5 + .5 * sin(p.x), 1.0);
}

片段着色器针对每个片段(像素)并行运行。在下面的示例中,片段着色器从 gl_FragCoord 变量读取以后像素坐标,并通过 gl_FragColor 中的 sin() 计算运行并输入色彩。

gl_FragColor 是一个 vec4 向量,其中蕴含(红色,绿色,蓝色,alpha),取值各为 0 .. 1。

GLSL 变量的类型

  • attribute: 顶点着色器从缓冲区中提取一个值并将其存储在属性变量中。
  • uniform: 从 JS 端设置对立变量。例如,您能够对立应用诸如以后鼠标、轻敲地位之类的内容传递给着色器。您还能够应用对立变量来拜访从 JavaScript 上传的纹理数据。
  • varying: 将值从顶点传递到片段着色器并进行插值。

上传图像数据

您能够应用图像数据拜访到着色器中的 WebGLRenderingContext,并将其上传到纹理中。(另请参见:WebGL 基础知识:图像处理)

您能够应用 texImage2D 外部办法 WebGLRenderingContext 将图像数据上传到纹理中。

// gl is the WebGLRenderingContext 
const texture = gl.createTexture()
gl.activeTexture(gl.TEXTURE0 + textureIndex);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);

// more info about these parameters in the webglfundamentals
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

您传递给 texImage2D 的图像数据,能够是 img 元素、视频元素、ImageData 等。

因为视频的图像数据一直变动,因而您必须在 requestAnimationFrame 动画循环内更新纹理。以下是获取实现的 texSubImage2D

gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, video);

读取纹理数据

在着色器代码中读取纹理数据,您能够通过 texture 的 2Dglsl 函数拜访纹理的像素数据。

当纹理坐标从(0,0)变为(1,1)时,图像会上下颠倒。同时,我正处于程度镜像图像中(就像用相机自拍一样)。

uniform sampler2D texture0;

void main() {vec2 coord = 1.0 - gl_FragCoord.xy / vec2(width, height);
  gl_FragColor = texture2D(texture1, coord);
}

拜访网络摄像头

要从网络摄像头获取图像数据,咱们能够应用 video 标签,并应用 getUserMediaAPI

function accessWebcam(video) {return new Promise((resolve, reject) => {
    const mediaConstraints = { audio: false, video: { 
        width: 1280, 
        height: 720,
        brightness: {ideal: 2} 
      }
    };

    navigator.mediaDevices.getUserMedia(mediaConstraints).then(mediaStream => {
        video.srcObject = mediaStream;
        video.setAttribute('playsinline', true);
        video.onloadedmetadata = (e) => {video.play();
          resolve(video);
        }
      }).catch(err => {reject(err);
      });
    }
  );
}

// 应用阐明:// const video = await accessWebcam(document.querySelector('video'));
// or via promises:
// accessWebcam(document.querySelector('video')).then(video => { ...});

要拜访网络摄像头,您能够应用 getUserMedia API 来拜访网络摄像头,如上所述。

提供后备图像

如果用户阻止了对网络摄像头的拜访,或者没有可用的网络摄像头,则能够提供一个备用图像供您应用。

我也将 new Image() 中的 onload 操作包装成一个 promise

function loadImage(url) {return new Promise((resolve, reject) => {const img = new Image();
    img.crossOrigin = 'Anonymous';
    img.src = url;
    img.onload = () => {resolve(img);
    };
    img.onerror = () => {reject(img);
    };
  });
}

合并全副操作

为了使事件变得容易一些,我将罕用的 WebGL 函数放入了我创立的一个小助手库 GLea 中。

它初始化 WebGL 利用上下文,编译 WebGL 着色器代码,并为顶点着色器创立属性和缓冲区:

默认状况下,position 为顶点着色器提供一个属性,该属性带有一个缓冲区,该缓冲区蕴含 4 个 2D 坐标,笼罩整个屏幕上的 2 个三角形。

import GLea from 'glea.js';

const frag = ` ... `; // 片段着色器代码
const vert = ` ... `; // 顶点着色器代码
const glea = new GLea({
  shaders: [GLea.fragmentShader(frag),
    GLea.vertexShader(vert)
  ]
}).create();

function loop(time = 0) {const { gl, width, height} = glea;
  glea.clear();
  glea.uniV('resolution', [width, height]);
  glea.uni('time', time * 1e-3);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(loop);
}

window.addEventListener('resize', () => {glea.resize();
});
loop(0);

论断

基本上就是这样。我心愿您喜爱浏览本文,并对本人摸索 WebGL 感到好奇。我会在这里放一些资源。

如果我还有没介绍到的内容,请随时发表补充评论 =)。

参考资料

  • https://webglfundamentals.org/
  • https://thebookofshaders.com/
  • https://frontendmasters.com/c…
  • https://codame.com/events/wor…

正文完
 0