高效率视频播放: GPU支持的YUV RGB 转化例子(2)

    最近一直在做视频的播放。尤其是HD的视频,即使是1024 x 576的视频播放在CPU消耗上是一个巨大的压力。更大的还有1920 x 1080的。在CPU的消耗上简直就是一个恶梦。
    最近用DSHOW做了一个Demo。一般Dshow的例子里都是直接用VMR把视频播放出去。对于游戏开发人员跟视频处理的应用来说,一个额外的要求就是你自己需要写一个Video Renderer截获视频的Frame,并把视频的帧处理后用3DAPI显示出来。
    做这个demo的过程中,碰到一个以前不愿意解决的问题就是DSHOW的输出的象素格式问题,以前我一直用RGB24,虽然在Linux的项目中也用过YUV。不过实在不愿意去处理。但是这回扛不住了,很多HD的解码器的filter。输出的只能是YV12跟YUY2格式的。而且即使能输出RGB的解码器,CPU占用率也非常的高,因为YUV->RGB转化占用了巨大的资源。看来把YUV-RGB模块放到GPU端是非做不可了。
    补充说明一点,使用CyberLink解码器,采用MS的YUV->RGB代码,在解码1280 x 576的H.264视频时候,CPU占用率为38%(Core2 E6300 2G DDR2). 采用GPU(GF 7600GS)时候CPU占用率约为20%. 播放1920 x 1080的时候CPU约为40%。我这里提供的方法并非为了转换而转换,而是为了播放视频而转换。
    这里介绍一下YV12和YUY2的基本知识。当然我假设你知道YUV是什么东西。简单的说,YUV的格式在存储上有两类布局: Packed和Plannar。Packed的方式就是把相邻几个象素打包起来。比如把水平方向2个象素打包到一个DWORD里。Planner方式则相反。Y分量和UV分量完全分开来保存。YUY2和YV12是最常用的两个代表。YUY2是packed方式的。水平方向两个像素打包到一个DWORD,并且UV采样率只有Y的一半,这符合人的视觉特征能有效的压缩数据,具体布局为[Y0, U0,Y1,V0]。 这种格式常见于MPEG1的解码器。YV12则常见于H.264的解码器,它属于plannar方式。对于一个MxN大小的视频来说,数据布局为[Y:M x N] [U:M/2 x N/2] [V:M/2 x N/2]. 也就是说UV的采样率在水平和垂直方向上都只有Y的一半。
   
    知道了数据格式以后,我们就可以出台正确的方针了。
    首先我们来看YV12。这个比较简单一些。创建文理的时候,我们创建3个纹理,像素格式均为D3DFMT_L8格式。
    代码如下:
    m_nVideoTexture = 3;
    m_pVideoTexture[0] = m_Device.createTexture(iWidth   , iHeight   , D3DFMT_L8);
    m_pVideoTexture[1] = m_Device.createTexture(iWidth/2 , iHeight/2 , D3DFMT_L8);       
    m_pVideoTexture[2] = m_Device.createTexture(iWidth/2 , iHeight/2 , D3DFMT_L8);

    每一个Frame填充数据的时候代码如下。
    const char* pTexData = (const char*)VideFrameData;
    m_Device.updateTextureData(m_pVideoTexture[0], pTexData , w   , h   , D3DFMT_L8);
    pTexData += w * h;
    m_Device.updateTextureData(m_pVideoTexture[1], pTexData , w/2 , h/2 , D3DFMT_L8);
    pTexData += w * h/4;
    m_Device.updateTextureData(m_pVideoTexture[2], pTexData , w/2 , h/2 , D3DFMT_L8);

   
    最后就是显示这三张Texture.
    因为第一张纹理是一张黑白的图象。也就是说只用第一个纹理。你的电影就是黑白片了。接下来我们需要有一个Pixel Shader在GPU中把YUV转换成RGB.shader如下。
    sampler2D YTextue;
    sampler2D UTextue;
    sampler2D VTextue;
    float4 main( Texcoord : TEXCOORD0 ) : COLOR0
    {     
       float3 yuvColor;
       float3 delYuv = float3(-16.0/255.0 , -128.0/255.0 , -128.0/255.0);
       yuvColor.x       = tex2D( YTextue, Texcoord ).x;
       yuvColor.y       = tex2D( UTextue, Texcoord ).x;
       yuvColor.z       = tex2D( VTextue, Texcoord ).x;
       yuvColor += delYuv;
       float3 matYUVRGB1 = float3(1.164,  2.018 ,   0.0   );
       float3 matYUVRGB2 = float3(1.164, -0.391 , -0.813  );
       float3 matYUVRGB3 = float3(1.164,    0.0 ,  1.596  );
       float4 rgbColor;
       rgbColor.x = dot(yuvColor,matYUVRGB1);
       rgbColor.y = dot(yuvColor,matYUVRGB2);
       rgbColor.z = dot(yuvColor,matYUVRGB3);
       rgbColor.w = 1.0f;
       return rgbColor;
    }
    因为RGB这三个纹理是独立缩放的,所以输入输出的大小不一样而产生缩放的时候不会对画面产生影响。只要简单的对齐Pixel到texel中心就可以了。


    接下来我们来处理YUY2格式。创建纹理的方式基本一致,因为水平方向两个像素打包成4个字节,所以纹理宽度是视频宽度的一半。代码如下
    m_nVideoTexture = 1;
    m_pVideoTexture[0] = m_Device.createTexture(iWidth/2, iHeight   , D3DFMT_L8);
    上传纹理数据和普通纹理没区别。
    最后是绘制。这是YUY2区别于YV12最多的一块,关键难点在于,如何将打包的像素解包的问题,我们可以根据输入的纹理坐标计算出对应视频的像素坐标。VideoPixel(x,y)=TexCoord(x,y)*VideoSize(w ,h); 然后根据VideoPixel.x的奇偶性来判断读取哪一个Y。
    上述想法有一个严重的问题就是假设输入尺寸和输出是完全一致的,这样才能保证PixelShader里读到的每一个纹理坐标对应的像素坐标都是个整数。这样就没办法处理缩放了。解决的办法就是创建一个和视频尺寸等大的RenderTarget。先绘制到RenderTarget。也就是说RenderTarget里得到的是一个RGB的图象。然后再绘制成你需要的尺寸。这样,创建代码就要相应改成如下:(注意,视频尺寸可能远远大于你的Backbuffer,所以你还需要创建一个DepthBuffer)。
   m_nVideoTexture = 1;
   m_pVideoTexture[0] = m_Device.createTexture(iWidth/2 , iHeight , D3DFMT_A8R8G8B8);
   m_pRenderTarget  = m_Device.createRenderTarget(iWidth   , iHeight);
   m_pDepthBuffer   = m_Device.CreateDepthStencilSurface(iWidth,iHeight,D3DFMT_D16);

   绘制代码则变成如下
   //绘制到RenderTarget里
   setupRenderTarget(m_pRenderTarget,m_pDepthBuffer);
   //YUV2RGB转化的Shader
   setupYUY2ToRGBShader();
   setupTexture(m_pVideoTexture[0]);
   drawQuad();
   setupShader(NULL);
   restoreRenderTarget();

   //转化完毕,绘制RGB
   setupTexture(m_pVideoTexture[0]);
   drawQuad();

   YUY2转换RGB的Shader如下:

   sampler2D YUY2Textue;
   float4    TexSize;

   float Mod2(int x)
   {
       int x2 = x/2;
       float ix2  = x2;
       float fx2   = x/2.0;
       if(ix2 == fx2)
             return 0;
        return 1; 
   }

   float4 main( float2 TexcoordIn:TEXCOORD0) : COLOR0
   {     
      float3 yuvColor;
      float3 delYuv    = float3(-16.0/255.0 , -128.0/255.0 , -128.0/255.0);
      float2 TexCoord = TexcoordIn;
      int texCoordX = TexCoord.x*TexSize.x*2;
      if( Mod2(texCoordX) > 0.0)
            yuvColor.x       = tex2D( YUY2Textue, TexCoord ).x;
      else
            yuvColor.x    = tex2D( YUY2Textue, TexCoord ).z;
      yuvColor.y          = tex2D( YUY2Textue, TexCoord ).w;
      yuvColor.z          = tex2D( YUY2Textue, TexCoord ).y;

      yuvColor += delYuv;
      float3 matYUVRGB1 = float3(1.164,  2.018 ,   0.0   );
      float3 matYUVRGB2 = float3(1.164, -0.391 , -0.813  );
      float3 matYUVRGB3 = float3(1.164,    0.0 ,  1.596  );

      float4 rgbColor;
      rgbColor.x = dot(yuvColor,matYUVRGB1);//yuvColor.x;//
      rgbColor.y = dot(yuvColor,matYUVRGB2);//yuvColor.x;//
      rgbColor.z = dot(yuvColor,matYUVRGB3);//yuvColor.x;//
      rgbColor.w = 1.0f;

      return rgbColor;
   }


未来的计划。
   1: 由于采用PixelShader做后期处理速度优势非常明显,可以考虑采用动态组合的方式来实现视频特效。
   2: 可以考虑把反交错也放到PS里去做。
   3: 没想好,但是要YY一下。
  
 

   
 

你可能感兴趣的:(图形)