最近一直在做视频的播放。尤其是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一下。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1821992