vr视频六面体转换的Equi-Angular优化以及实现

本篇主要基于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直接映射到对应即可,不需要考虑扩充系数。

通用方法

其实和贴全景vr视频(球膜)方法基本相同,将输入图像uv按一定间距分割,然后每个点找对应映射的coords三维坐标位置,最后放好画图顺序。这个方法的缺点是,划分不够细的话,各个点之间不是等角的,每个点之间其实是线性的。划分太多,则shader输入点的个数太多,影响播放效率。

具体做法和播放球面类似,就不赘述了,这里贴下简单的代码,以供参考逻辑:

	// 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之前就全部完成了,所以这里不涉及任何三维空间。


把归一化等等都写到fragment shader里(其中1.01是映射时用的expand_coef。如果映射时采用其他值,这里的值也需要对应改):

    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全景的某个角度:





  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值