本篇主要基于facebook之前提出的vr视频转cube六面体的方案,根据文章里的优化算法对其进行优化,并实现播放。
关于之前六面体的映射方法,facebook早早就提供了基于ffmpeg的滤镜vf_transform.c,其他相关请参考我另一篇笔记:
http://blog.csdn.net/defence006/article/details/52459543
这里主要是针对cube映射的优化,等角映射。原理,效果等等请参考原文:
http://www.googblogs.com/author/chip-brown/
总结其优点就是,相比原来的cube映射,Equi-Angular cube映射可以保证等角度的映射,从而保证了cube每个面上对全景视频采样的均匀性,提高主观质量。相对于原始的cube,每个正方形正面的采样会更多,所以更清晰。如下图:
代价的话,由于每个面上每个像素是按等角去映射的,所以播放的时候不能简单的把正方形贴上去,而是需要对每个正方形上的点进行坐标转换后再映射。
实现这种新的六面体映射有两个部分:1,全景图映射到等角六面体,2,等角六面体在播放端播放
映射Equi Angular Cube
基于以前facebook提供的vf_transform.c进行少量改动即可。
首先,根据等角度映射的出发点,投影正方形上每个像素对应的位置之间应该是等距的。考虑我们映射cube中某个正方形x轴从左到右的一条线(y轴任意值不考虑),我们希望每个像素对应的坐标x_pxl (范围0~1),转换之后得到等角度的坐标x_agl(范围0~1)。
那么: x_agl = tan((x_pxl -0.5)*M_PI/0.5/4) / 2 + 0.5;
相当于从 (-Pi/4) 到( Pi/4)之间平分了90度的角度,然后归一化到(0~1)的范围。考虑播放端天空盒各个面拼接缝隙的问题,需要在增加一个参数expand_coef扩充采样范围。表示放大到原来的多少倍,分辨率够高的话取个1.01包含边缘就可以。
x_agl = tan((x_pxl -0.5)*M_PI/0.5/4) * expand_coef / 2 + 0.5;
基于vf_transform.c,只需要对原来transform_pos()函数中稍作修改即可。
将原来的代码:
x = (x - 0.5f) * expand_coef + 0.5f;
y = (y - 0.5f) * expand_coef + 0.5f;
替换为等角度映射:
x = tan((x-0.5)*M_PI/0.5/4) * expand_coef / 2 + 0.5;
y = tan((y-0.5)*M_PI/0.5/4) * expand_coef / 2 + 0.5;
EAC:
全景播放Equi Angular Cube视频
有两种方法,一种方法是通用方法,根据映射时采用的模型,在播放时进行同样的映射,把六个面放置在opengl空间中,然后拆分成无数小三角,丢给shader去贴图。shader只做拷贝和贴图,没有任何特殊处理。另一种方法,所有的映射都交给fragment shader去处理,而贴图仅仅是将输入的uv六个面,分别贴到三维空间中立方体的六个面。注意,expand_coef也交给fragment shader去做,所以vertex和coords直接映射到对应即可,不需要考虑扩充系数。
通用方法
具体做法和播放球面类似,就不赘述了,这里贴下简单的代码,以供参考逻辑:
// input x, y: uv position.
// output locX, locY, locZ: vertex position
// return: cube face
private int transform_pos_face(float x, float y)
{
......
vface = (int) (y * 2);
hface = (int) (x * 3);
x = x * 3.0f - hface;
y = y * 2.0f - vface;
face = hface + (1 - vface) * 3;
x = (float) ((Math.tan((x-0.5)*Math.PI/0.5/4)) * expand_coef / 2 + 0.5);
y = (float) ((Math.tan((y-0.5)*Math.PI/0.5/4)) * expand_coef / 2 + 0.5);
switch (face) {
case RIGHT: p = P5; vx = NZ; vy = PY; break;
case LEFT: p = P0; vx = PZ; vy = PY; break;
case TOP: p = P6; vx = PX; vy = NZ; break;
case BOTTOM: p = P0; vx = PX; vy = PZ; break;
case FRONT: p = P4; vx = PX; vy = PY; break;
case BACK: p = P1; vx = NX; vy = PY; break;
}
locX = p [0] + vx [0] * x + vy [0] * y;
locY = p [1] + vx [1] * x + vy [1] * y;
locZ = p [2] + vx [2] * x + vy [2] * y;
return face;
}
// calculate uv coords(2D) VS vertexs(3D)
for(v = 0; v < height; v++) {
for(h = 0; h < width; h++) {
xh = (float) ((h + 0.5) / width);
yv = (float) ((v + 0.5) / height);
face = transform_pos_face(xh, yv);
faceIdx[k++] = face;
if (faceOffset[face] == 0xffff) {
faceOffset[face] = v * width + h;
Log.v(TAG, "chao, face: " + face + " v,h "+ v +"," +h);
}
x = locX;
y = locY;
z = locZ;
texcoords[t++] = xh;
texcoords[t++] = yv;
vertexs[m++] = x * radius;
vertexs[m++] = y * radius;
vertexs[m++] = z * radius;
}
}
// draw order
for (face = 0; face < 6; face ++)
{
fOffset = faceOffset[face];
for (v = 0; v < step; v++) {
for (h = 0; h < step; h++) {
indices[counter++] = (fOffset + v * wStep + h); //(a)
indices[counter++] = (fOffset + (v+1) * wStep + (h)); //(b)
indices[counter++] = (fOffset + (v) * wStep + (h+1)); // (c)
indices[counter++] = (fOffset + (v) * wStep + (h+1)); // (c)
indices[counter++] = (fOffset + (v+1) * wStep + (h)); //(b)
indices[counter++] = (fOffset + (v+1) * wStep + (h+1)); // (d)
}
}
}
由fragment shader来处理等角度映射到对应坐标的关系
优点是不需要考虑对uv分割的数目,同时vertex很少,只有六个面共24个点。同时速度也比较快。采用这种方法时,uv坐标对应opengl空间的对应贴图直接按理想cube贴图,不需要考虑expand_coef或者其他因素。比如左上角的正方形顶点对应关系(正方形边长取1时):
coords(0.0 0.0) -> vertex (-0.5, -0.5, -0.5)
coords(0.33333334 0.0) -> vertex ( 0.5, -0.5, -0.5)
coords(0.0 0.5) -> vertex (-0.5, -0.5, 0.5)
coords(0.33333334 0.5) -> vertex ( 0.5, -0.5, 0.5)
fragment shader 对光栅化处理后生成的片元逐个进行处理,相当于对各个光栅化后的像素逐个处理。到这里已经完成了在opengl三维空间的投影,frustum等等,已经准备画出输出的二维图像了。所以这里已经没有三维空间,只有准备输出的二维图像,以及作为输入的二维图像。对此,我们需要先将输入的六面体图像分割成6个正方形,然后对各个正方形坐标归一化,最后根据当前像素坐标映射到实际输出的点的坐标,最后取得该点的color输出。
坐标对应映射公式为(注意expand_coef的位置是根据前面映射时放置的位置,倒着推出来,其值和编码时映射时采用的expand_coef相同):
x = (float) (Math.atan2(x-0.5, 0.5*expand_coef) * 2 / Math.PI + 0.5);
y = (float) (Math.atan2(y-0.5, 0.5*expand_coef) * 2 / Math.PI + 0.5);
这里,左边的图,表示的是输入二维图像uv中,切割出来的一个正方形并对其做归一化处理。右面的图是在fragment shader贴图时映射的二维图像。三维空间的映射早在进入fragment shader之前就全部完成了,所以这里不涉及任何三维空间。
private final String mFragmentShader =
"#extension GL_OES_EGL_image_external : require\n" +
"precision highp float;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform samplerExternalOES sTexture;\n" +
"vec2 vTmpCoord;\n" +
"void main() {\n" +
"vTmpCoord.xy = vTextureCoord.xy;\n" +
"if (vTmpCoord.x < 0.33333333)\n" +
"{\n" +
" if (vTmpCoord.y < 0.5) {" +
" vTmpCoord.x = ( atan( 2.0 * (vTmpCoord.x - 0.16666667) / 0.33333333 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.33333333;\n" +
" vTmpCoord.y = ( atan( 2.0 * (vTmpCoord.y - 0.25) / 0.5 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.5;\n" +
" }\n" +
" else\n" +
" {\n" +
" vTmpCoord.x = ( atan( 2.0 * (vTmpCoord.x - 0.16666667) / 0.33333333 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.33333333;\n" +
" vTmpCoord.y = ( atan( 2.0 * (vTmpCoord.y - 0.75) / 0.5 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.5 + 0.5;\n" +
" }\n" +
"}\n" +
"else if (vTmpCoord.x < 0.66666667)\n" +
"{\n" +
" if (vTmpCoord.y < 0.5)\n" +
" {\n" +
" vTmpCoord.x = ( atan( 2.0 * (vTmpCoord.x - 0.5) / 0.33333333 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.33333333 + 0.33333333;\n" +
" vTmpCoord.y = ( atan( 2.0 * (vTmpCoord.y - 0.25) / 0.5 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.5;\n" +
" }\n" +
" else\n" +
" {\n" +
" vTmpCoord.x = ( atan( 2.0 * (vTmpCoord.x - 0.5) / 0.33333333 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.33333333 + 0.33333333;\n" +
" vTmpCoord.y = ( atan( 2.0 * (vTmpCoord.y - 0.75) / 0.5 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.5 + 0.5;\n" +
" }\n" +
"}\n" +
"else\n" +
"{\n" +
" if (vTmpCoord.y < 0.5)\n" +
" {\n" +
" vTmpCoord.x = ( atan( 2.0 * (vTmpCoord.x - 0.83333333) / 0.33333333 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.33333333 + 0.66666667;\n" +
" vTmpCoord.y = ( atan( 2.0 * (vTmpCoord.y - 0.25) / 0.5 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.5;\n" +
" }\n" +
" else\n" +
" {\n" +
" vTmpCoord.x = ( atan( 2.0 * (vTmpCoord.x - 0.83333333) / 0.33333333 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.33333333 + 0.66666667;\n" +
" vTmpCoord.y = ( atan( 2.0 * (vTmpCoord.y - 0.75) / 0.5 / 1.01 ) * 2.0 / 3.14159265 + 0.5 ) * 0.5 + 0.5;\n" +
" }\n" +
"}\n" +
" gl_FragColor = texture2D(sTexture, vTmpCoord);\n" +
"}\n";
这里有个坑,精度一定要保证 precision highp float,不然输出结果接缝处会有缝。
以下是手机端播放Equi Angular Cube全景的某个角度: