Yuv420P格式在安防视频中非常常见,因为H264或者H265解码之后,就是这种格式。

YUV定义了三个分量:“Y”表示明亮度(Luminance或Luma)也就是灰度值。U和V表示色度。

即我们的Yuv420P会保存3种分量的数据。这里以一个4*4的图像为例,其保存的方式。

Y0

Y1

Y2

Y3

Y4

Y5

Y6

Y7

Y8

Y9

Y10

Y11

Y12

Y13

Y14

Y15

U0

U1

U2

U3

V0

V1

V2

V3

我们用一个直接数组表示的话,其在内存里面的存储如下:

let data = new Uint8Array([
Y0,Y1,Y2,Y3,
Y4,Y5,Y6,Y7,
Y8,Y9,Y10,Y11,
Y12,Y13,Y14,Y15,
U0,U1,U2,U3,
V0,V1,V2,V3])

YUV420P的格式表示我们每采样4个Y值,才会采样一个U值和一个V值。

webgl渲染

我们先研究如何用webgl渲染一个三角形。webgl渲染最核心的是两个着色器:顶点着色器和片段着色器。其他都是围绕着如何给这两个着色器赋值来展开。

顶点着色器:就是告诉webgl我需要绘制的三角形的顶点的位置

片段着色器:告诉webgl需要绘制的三角形每个像素颜色

当我们把顶点缓存和颜色缓存设置好了之后,就可以通过drawArrays来驱动webgl来绘制了。

所以我们先看驱动函数:

      var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);

这里告诉我们告诉webgl我们需要绘制一个三角形,count=3表示绘制了3个顶点。

我们设置的顶点数据如下:

      var positions = [
0, 0,
0, 1,
0.5, 0,
];

再看我们的顶点着色器

  <script id="vertex-shader-2d" type="notjs">
attribute vec4 a_position;
void main() {
gl_Position = a_position;
}
</script>

这里的顶点着色器会被调用三次,a_position的值分别为[0, 0],[0, 1],[0.5, 0]。通过gl_Position传给webgl。

顶点着色器完了之后,就会调用片段着色器,主要给像素设置颜色。

  <script id="fragment-shader-2d" type="notjs">
precision mediump float;
void main() {
gl_FragColor = vec4(1, 0, 0.5, 1);
}
</script>

这里我们直接赋值了一个红色。所以看下最终效果:我们成功绘制了一个红色的三角形。

webgl渲染Yuv420P图像_webgl

有了绘制三角形的思路,我们可以联想到如何绘制YUV420P的图像。我们猜测可以准备4个顶点,绘制两个三角形,然后再改造片段着色器给两个三角形的像素赋予颜色应该就可以绘制出一幅图像。实际上就是按照这个思路搞定。

片段着色器wengl提供了纹理坐标来获取到对于的图像位置的颜色。即通过texture2D函数配合纹理坐标可以从一幅图像中获取到颜色。我们需要做的只是设置好纹理坐标和顶点坐标的映射关系即可。

看下图:

webgl渲染Yuv420P图像_js_02

我列了个表格,以表示纹理坐标和顶点坐标的对于关系

纹理

顶点

实际顶点

1.0 ,0.0

1.0,-1.0

1.0, 1.0

0.0,0.0

-1.0,-1.0

-1.0, 1.0

1.0,1.0

1.0,1.0

1.0, -1.0

0.0,1.0

-1.0,1.0

-1.0, -1.0

为什么要写一个实际顶点坐标呢?因为webgl绘制的时候,需要将Y的坐标取反,不然图像显示不对,应该就是这么规定的。

接下来我们准备好顶点坐标和纹理坐标:看我们下面代码中的数组,其实和上面的表格的值是一一对应的。

  var verticesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER,
new Float32Array([1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0]),
gl.STATIC_DRAW);
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER,
new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]),
gl.STATIC_DRAW);
gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);

再到我们的驱动函数:drawArrays。

gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

上面我们设置了4个顶点,所以他就绘制了4次。再到顶点着色器:

  var vertexShaderSource = [
"attribute highp vec4 aVertexPosition;",
"attribute vec2 aTextureCoord;",
"varying highp vec2 vTextureCoord;",
"void main(void) {",
" gl_Position = aVertexPosition;",
" vTextureCoord = aTextureCoord;",
"}"
].join("\n");

顶点着色器会被调用4次,将4个顶点坐标传递给webgl的gl_Position。这里将纹理坐标复制给了vTextureCoord对象,这个是给下面的片段着色器使用的。

接下来进入重点的重点,片段着色器:

  var fragmentShaderSource = [
"precision highp float;",
"varying lowp vec2 vTextureCoord;",
"uniform sampler2D YTexture;",
"uniform sampler2D UTexture;",
"uniform sampler2D VTexture;",
"const mat4 YUV2RGB = mat4",
"(",
" 1.1643828125, 0, 1.59602734375, -.87078515625,",
" 1.1643828125, -.39176171875, -.81296875, .52959375,",
" 1.1643828125, 2.017234375, 0, -1.081390625,",
" 0, 0, 0, 1",
");",
"void main(void) {",
" gl_FragColor = vec4( texture2D(YTexture, vTextureCoord).x, texture2D(UTexture, vTextureCoord).x, texture2D(VTexture, vTextureCoord).x, 1) * YUV2RGB;",
"}"
].join("\n");

这里主要实现了两点功能,一个是将YUV换算成rgb。一个是对Y,U,V各个分量进行纹理采样。

在webgl里面,yuv转rgb的公式如下:

r = y * 1.1643828125 + 1.59602734375 * v - 0.870787598;             
g = y * 1.1643828125 - 0.39176171875 * u - 0.81296875 * v + 0.52959375;
b = y * 1.1643828125 + 2.01723046875 * u - 1.081389160375;

我们换算成矩阵的形式:

[r,g,b] =

[y,.u,v,1]    *

[1.1643828125, 1.1643828125 ,  1.1643828125

1.59602734375,-0.39176171875,  2.01723046875

0,            -0.81296875,     0

-0.870787598, 0.52959375,      - 1.081389160375]

我们在看上面的webgl的矩阵:

"const mat4 YUV2RGB = mat4",
"(",
" 1.1643828125, 0, 1.59602734375, -.87078515625,",
" 1.1643828125, -.39176171875, -.81296875, .52959375,",
" 1.1643828125, 2.017234375, 0, -1.081390625,",
" 0, 0, 0, 1",
");"

我们会发现上述矩阵的行变成webgl定义矩阵的列,其实这是一种固定写法,表达的矩阵还是我们上面的矩阵。

搞定了变换矩阵,我们就是Y,U,V的纹理采样了。我们通过设置了3个不同的纹理,来把YUV的数据传递给webgl。通过最开始研究YUV420P的内存格式,我们可以将YUV的数据进行分离。

Y = videoFrame.subarray(0, width*height)
U = videoFrame.subarray( width*height, width*height+ width*height/4)
V = videoFrame.subarray(width*height+ width*height/4, width*height+ width*height/2)

将分离的数据在赋值给纹理即可。

到这里,基本上webgl渲染YUV420P的思路讲完了,总结一下:

1、设置顶点坐标---》4个顶点

2、设置纹理坐标---》4个纹理坐标

3、创建3个纹理---》Y,U,V

4、在片段着色器里面进行矩阵变换,将YUV转成RGB

最后上一张效果图:上面的是webgl渲染的,下面的是使用yuv工具渲染的,效果基本上一样。

webgl渲染Yuv420P图像_canvas_03

webgl渲染Yuv420P图像_shader_04