OpenGl ES渲染画面--放大和缩小

通常我们在iOS(或Android)上通过OpenGl ES来播放视频时,除了需要画面能够正常播放外,可能还有一些其他的需求,比如增加滤镜、调整色值、画面进行缩放等等各种各样的需求。当然,不管是什么样的需求,总是离不开对于着色器、纹理坐标、顶点坐标扽等的操作。今天,我们就来说一说,如何对正在播放的视频进行缩放。

可能有一部分同学会说,那我们直接更改画面的图层Frame,进行放大、缩小不就好了?如果需要拖动再加上ScrolView不就好了?

理论上来说,这样确实可以近似实现我们的需求,但是,当你真正了解了视频画面的播放、渲染的流程后,就不会这样认为了。

1、通过改变图层Frame来实现为什么不行?

首先,我们可以来看一下为什么通过直接操作视频画面图层的大小来实现画面的缩放是不可以的(或者说是不合理的)。

图1

正常情况下,视频画面图层刚好铺满整额窗口,渲染区域正好是整个图层的大小。而当不断放大图层Frame时,渲染的区域则也会对应的变大。如上图,放大后,实际上是把整个图层放大了,我们能看的区域始终是窗口的大小,而渲染的区域却会随Frame的变大而变大,屏幕外的区域虽然我们看不到,但是还是会去渲染的。

我们都知道视频最终展现在界面上,是需要CPU、GPU共同处理数据、经过渲染、提交到屏幕显示的,我们需要显示的区域越大,则对于CPU、GPU的压力会越大,当到达他们能够处理的上限后,肯定就会出现异常。

如下代码主要是创建缓冲区,并指定渲染图层:

   //创建帧缓冲区、渲染缓冲区并进行绑定

   glGenFramebuffers(1, &_framebuffer);

    glGenRenderbuffers(1, &_renderBuffer);

    //绑定

    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);

    glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);

    //指定当前需要显示画面的图层,如果改变图层的大小,则一直会触发并进入此方法,并更新需要渲染的区域

    if (![_glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer]) {

        NSLog(@"attach渲染缓冲区失败");

    }

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {

        NSLog(@"创建缓冲区错误 0x%x", glCheckFramebufferStatus(GL_FRAMEBUFFER));

    }

当我们的layer的frame不断变大到一定程度后,到达内存的瓶颈后(这里使用iPhoneX全屏播放,放大3倍左右就到大瓶颈了),在这里创建缓冲区会直接失败(这里使用的OpenGl ES 2,如果是OpenGl ES 3则会直接Crash),则画面会直接静止,不会继续播放了。

所以说,通过直接改变图层Frame的方式来实现缩放功能,不仅造成了对资源的浪费,而且对放大的倍数也有限制,是不合理的。

重点来了........

那如何才能行呢?如何才能实时去缩放画面还能不造成对资源的浪费呢?能否无限制的放大画面呢?

接下来,我们来看看如果通过OpenGl来实现画面的缩放、拖动

2、通过OpenGl实现视频画面的缩放、拖动

我们要做到不浪费资源、用户看到的窗口是多大哪我们就渲染多大的区域,那图层的layer的Frame就基本不变,只需要将窗口中画面不同的区域进行放大即可。

实现原理,根据放大倍数的不同、显示的画面位置不同,在渲染画面的窗口中映射到不同的区域来显示画面,通过上层算法来计算映射坐标、区域等参数。由于需要将视频窗格的坐标系数据映射到OpenGl窗口中,所以需要进行坐标转换。

1、在此之前,先说明下OpenGl的坐标系,这里主要说2D下的坐标系数据,纹理坐标、顶点坐标。我们都知道UIKit的坐标系,(0,0)点在屏幕左上角,而OpenGl的顶点坐标系的原点(0,0)在屏幕中央,(-1,-1)在左下角,(1,1)在右上角,如图:  

图1

       1) 该坐标系为顶点坐标系,代表画面在窗格中显示的区域,一般顶点坐标系数组固定为 

        { 

                -1.0f, 1.0f, 

                1.0f, 1.0f, 

                -1.0f, -1.0f, 

                1.0f, -1.0f, 

        } 

分别表示左上、右上、左下、右下角的顶点坐标。 

        2)接下来,就是纹理坐标系,它的(0,0)点在屏幕的左下角,刚好和UIKit下坐标系上下相反,坐标为 

        { 

                0.0f, 1.0f, 

                1.0f, 1.0f, 

                0.0f, 0.0f, 

                1.0f, 0.0f, 

        } 

同样,分别表示左上、右上、左下、右下角的纹理坐标。 


2、放大局部画面的话,实际上是改变画面在纹理坐标系中的映射位置。

可以想象下,未放大时,窗口刚好和纹理坐标系大小重叠,当需要放大画面时,将窗口映射缩小到纹理坐标系内不同的位置,即可实现局部画面放大到整个窗口上。如图:

图2

可以看到,通过上面四次放大,窗口聚焦到了蓝色区域(整个灰色区域可以看作是纹理坐标区域),而窗口的大小实际上并未改变,结果就是将蓝色区域放大到了整个窗口上,这就达到了放大的效果。在这个过程中我们实际上是改变了纹理坐标的值,而实际上最终渲染的区域也是我们指定的区域,因此也不会出现资源浪费的问题,后面会详细说明。

通过上面的图,我们也可以联想到,如果放大2倍(比如画面中心点,即纹理坐标区域中心点(0.5,0.5)不变的情况下),那么纹理坐标的值就变成了

{

0.25f, 0.75f,

0.75f, 0.75f,

0.25f, 0.25f,

0.75f, 0.25f

}

所以,若放大倍数是scale,则半屏宽度、高度,可以用一下公式得到:

float deltaX = (1.0f / scale) / 2;

接着,如果我们知道显示在屏幕窗口的区域的中心点,实际在纹理坐标系中的位置(x, y)的话,则可以通过以下代码来得到纹理坐标:

/**

 设置放大比例,x,y 是希望在屏幕上放大的区域的中心点

 **/

- (void)scaleWithScaleRatio:(GLfloat)scaleRatiox:(GLfloat)xy:(GLfloat)y//设置放大比例

{

    UVILog(@"scaleRation:%f ,x=%f,y=%f", scaleRatio, x, y);

    centerPoint.x = x;

    centerPoint.y = y;

    GLfloatcoord[8];

    if (scaleRatio <=1.0f) {

        centerPoint.x=0.5f;

        centerPoint.y=0.5f;


        UVDLog(@"scaleRatio==1.0f");

        memcpy(coordVertices, coordVertices_init, sizeof(coord));

        //    更新放大区域

        glVertexAttribPointer(ATTRIB_TEXTURE,2,GL_FLOAT,0,0,coordVertices);


        glEnableVertexAttribArray(ATTRIB_TEXTURE);

        mScaleRatio=1.0f;

        return; //注意,因为此时给了初始值,不需要再调用SetPosition了,直接返回

    } else {

        mScaleRatio= scaleRatio;

        //计算比例关系

        floatdeltaX = (1.0f/ scaleRatio) /2;

        floatdeltaY = (1.0f/ scaleRatio) /2;

        //计算四个点的坐标,给定的点是中心点(x, y),

        floatleftX, leftY;

        leftX = x - deltaX;//左上角X

        leftY = y + deltaY;//左上角Y

       //处理边界值

        if(leftX <0.0f) {

            //X,Y 太靠左,向右移

            leftX =0.0f;

        }

        elseif((x + deltaX  )>1.0f) {

            //太靠右,向左移。

            leftX =1.0f-2*deltaX;

        }

        if(leftY >1.0f) {

            //Y 太靠上

            leftY =1.0f;

        }

        elseif((y - deltaY ) <0.0f) {

            //太靠下

            leftY =0.0f+2*deltaY;

        }

        coord[0] = leftX;//左上角

        coord[1] = leftY;

        coord[2] = leftX +2*deltaX;//右上角

        coord[3] = leftY;

        coord[4] = leftX;//左下角

        coord[5] = leftY -2*deltaY;

        coord[6] = leftX +2*deltaX;//右下角

        coord[7] = leftY -2*deltaY;


        for(inti=0;i<8;i++)

        {

            if(coord[i]<0.0f)

            {

                coord[i]=0.0f;

            }

            elseif(coord[i]>1.0f)

            {

                coord[i]=1.0f;

            }

        }

        NSLog(@"scaleRatio:%f ,leftX=%f,leftY=%f,deltaX:%f,deltaY:%f", scaleRatio,leftX,leftY,deltaX,deltaY);

    }

    [self setPositionWithCoord:coord];

}

- (void)setPositionWithCoord:(GLfloat *)coord {

    int length = 8; //(int)sizeof(coordVertices)/GLfloat;

    if(coord !=NULL) {

        for(inti =0; i < length; i++) {

            GLfloatvalue = *(coord);

            if(i %2==1) {

                //因为纹理坐标系与视频UIKit坐标系上下相反的对应关系,需作处理

                value =1- value;

            }

            coordVertices[i] = value;

            coord++;

        }

    }

    //    更新放大区域

    glVertexAttribPointer(ATTRIB_TEXTURE, 2, GL_FLOAT, 0, 0, coordVertices);

    glEnableVertexAttribArray(ATTRIB_TEXTURE);

}

上面就是如何直接通过OpenGl来放大、缩小画面,而拖动画面,则可以直接改变上述的(x, y)的坐标即可


 3、OpenGl中放大原理,通过放大倍数、画面中心坐标三个参数实现画面放大: 

        1)将窗格画面的中心点(即UIKit中的坐标(x,y))转化映射到(0,1)坐标系中,定义为(glx,gly)。 

        2)计算半屏宽、高。定义放大倍数a,则(1/a)/2,即为半屏宽、高。 

        3)通过中心点(glx,gly),加、减对应的半屏宽、高,则可以获得纹理坐标。将此坐标绑定到顶点坐标系中,则实现了画面的放大、缩小 

(注意:纹理坐标需要处理边界数据;y轴的坐标UIKit和OpenGl相反,需进行处理,如上述方法- (void)setPositionWithCoord:) 


 4、手势缩放,通过UIKit坐标(x,y)实时计算纹理坐标系的中心点坐标(glx,gly): 

        1)通过缩放中心点(x,y)在整个放大后的画面中所占的比例来计算渲染中心点(glx,gly)时,在二次缩放后,缩放中心(x,y)可能变化很大,通过这种计算方式,会导致渲染中心点(glx,gly)的值发生大的变化,最终会表现出来画面跳动、漂移的问题,即缩放过程不连续。 

        为避免此问题,采用增量更新的方式,计算渲染中心点(glx,gly)。即每次缩放时,使用上一次的渲染中心点(glx,gly),加上本次缩放时,画面中心的偏移量,来计算新的渲染中心点。由于每次的缩放间隔为0.05,因此增量很小,缩放过程很平缓。 

        2)增量计算原理,手指在窗格上的缩放中心点为A点,B点为缩放前窗格中心点(在OpenGl坐标系中为B1点),缩放后,保持A点不变,此时窗格中心B点在OpenGl坐标系中对应B2点,(B2-B1)即为一次缩放的坐标增量,加上(glx,gly)就得到了新的OpenGl中心点 

(注:具体的算法详见代码) 


 5、数字放大后,拖动画面实时计算新的渲染中心点(glx,gly): 

        1)实现方式和上述类似,通过计算在OpenGl中的中心点坐标偏移量,加上拖动前的渲染中心点(glx,gly),得到最新的中心点 

        2)例如,x方向偏移量为dx,窗格宽为width,放大倍数为a,在OpenGl中的偏移量为(dx/(width*a)),则glx -=(dx/(width*a))。同理,可计算gly(注意:glx和gly的坐标方向相反) 

        综述,数字放大实现的核心处理如上,详细算法见代码。 

        另外,关于数字放大,主要关注缩放、拖动过程的连续性、平缓性,尤其是二次缩放、拖动。

通过这种方式放大画面后,将不受到GPU的限制,因为我们渲染的Frame始终未变,只要我们的画质够高,则可以放大任意倍数,几十倍都OK。

核心算法详见GitHub代码:

https://github.com/JJzzy/ZZVideoScale

你可能感兴趣的:(OpenGl ES渲染画面--放大和缩小)