【C++】从零开始,只使用FFmpeg,Win32 API,实现一个播放器(二)
前情提要
前篇:https://www.cnblogs.com/judgeou/p/14724951.html
上一集我们攻略了硬件解码 + Direct3D 9 渲染,这一整篇我们要搞定 Direct3D 11 的渲染,比9复杂的不是一点半点,因为将会涉及比较完整的图形管线编程,并且需要编写简单的着色器代码。关于图形学的内容我不会太深入(我也不懂啊哈哈),仅描述必要知道的知识点。
初始化D3D11
#include <d3d11.h> #pragma comment(lib, "d3d11.lib") // ... ShowWindow(window, SW_SHOW); // D3D11 DXGI_SWAP_CHAIN_DESC swapChainDesc = {}; auto& bufferDesc = swapChainDesc.BufferDesc; bufferDesc.Width = clientWidth; bufferDesc.Height = clientHeight; bufferDesc.Format = DXGI_FORMAT::DXGI_FORMAT_B8G8R8A8_UNORM; bufferDesc.RefreshRate.Numerator = 0; bufferDesc.RefreshRate.Denominator = 0; bufferDesc.Scaling = DXGI_MODE_SCALING_STRETCHED; bufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED; swapChainDesc.SampleDesc.Count = 1; swapChainDesc.SampleDesc.Quality = 0; swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapChainDesc.BufferCount = 2; swapChainDesc.OutputWindow = window; swapChainDesc.Windowed = TRUE; swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; swapChainDesc.Flags = 0; UINT flags = 0; #ifdef DEBUG flags |= D3D11_CREATE_DEVICE_DEBUG; #endif // DEBUG ComPtr<IDXGISwapChain> swapChain; ComPtr<ID3D11Device> d3ddeivce; ComPtr<ID3D11DeviceContext> d3ddeviceCtx; D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, NULL, NULL, D3D11_SDK_VERSION, &swapChainDesc, &swapChain, &d3ddeivce, NULL, &d3ddeviceCtx); // ...
d3d11 现在分了三个对象去控制图形操作,
IDXGISwapChain代表交换链,决定了你的画面分辨率,
Present也是在这个对象上面调用的。
ID3D11Device负责创建资源,例如纹理、Shader、Buffer 等资源。
ID3D11DeviceContext负责下达管线命令。
flags 设置为
D3D11_CREATE_DEVICE_DEBUG之后,如果d3d发生异常错误之类的,就会在 VS 的输出窗口直接显示错误的详细信息,非常方便。
(**注意:**使用
D3D11_CREATE_DEVICE_DEBUG需要安装 DirectX SDK,当你发布到别的电脑中运行时,请去除
D3D11_CREATE_DEVICE_DEBUG,否则会因为对方没有调试层而创建d3d设备失败。现在 DirectX SDK 其实已经木有了,Windows 10 SDK 其实就包含了原来的 DirectX SDK)
例如我把
swapChainDesc.BufferCount改为 1,调用
D3D11CreateDeviceAndSwapChain之后就会看到输出显示:
DXGI ERROR: IDXGIFactory::CreateSwapChain: Flip model swapchains (DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL and DXGI_SWAP_EFFECT_FLIP_DISCARD) require BufferCount to be between 2 and DXGI_MAX_SWAP_CHAIN_BUFFERS。。。
意思是当使用了
DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL或者
DXGI_SWAP_EFFECT_FLIP_DISCARD时,
BufferCount数量必须是 2 至
DXGI_MAX_SWAP_CHAIN_BUFFERS之间。BufferCount 就是后缓冲数量,增加缓冲数量能防止画面撕裂,但会加大显存占用以及增加延迟。
如果平时有用 PotPlayer,那么在 视频渲染器 设置里面的 Direct3D显示方式 选项,对应的正是
DXGI_SWAP_EFFECT的各个枚举值
enum DXGI_SWAP_EFFECT { DXGI_SWAP_EFFECT_DISCARD = 0, DXGI_SWAP_EFFECT_SEQUENTIAL = 1, DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL = 3, DXGI_SWAP_EFFECT_FLIP_DISCARD = 4 };
如果对相关内容十分感兴趣,可以阅读这篇文章:For best performance, use DXGI flip model。简单总结,就是请尽可能使用 Flip 模型。
渲染一个四边形
现在,我们先把FFmpeg放一边,学学 DirectX 图形编程,相信我,这就是这篇教程最难的部分,如果你能完全搞明白,后面的部分对你来说绝对是小意思。
Direct3D 11 图形管线有很多阶段,但我们不需要每一阶段都用上,以下就是我们必须编程的阶段:
- Input-Assembler Stage(输入装配)
- Vertex Shader Stage (顶点着色器)
- Rasterizer Stage (光栅化)
- Pixel Shader Stage (像素着色器)
- Output-Merger Stage (输出合并)
完整的管线阶段看这个图(不看也行):
GPU需要经历若干个阶段才能最终熬制1帧画面,每一个阶段都需要上一个阶段的运行结果作为参数输入,同时也可能需要额外加入新的输入参数。
我们新增一个函数 Draw 来实现上面必经阶段:
void Draw(ID3D11Device* device, ID3D11DeviceContext* ctx, IDXGISwapChain* swapchain) { // 顶点输入 // ... // 顶点索引 // .. // 顶点着色器 // ... // 光栅化 // ... // 像素着色器 // ... // 输出合并 // ... // Draw Call // ... // 呈现 // ... }
顶点输入
一个四边形有4个顶点,假设是一个边长为 2 的正方形,中心点坐标是(0,0),那么四个角的坐标很容易就可以得出,如图所示:
但是 dx11 不支持直接绘制四边形,只能选择绘制三角形,所以我们需要绘制两个直角三角形,它们拼到一起之后,自然就是一个四边形了。这个时候,顶点数量就从4个,变成了6个,但有两个点是完全重合的,dx11 提供了这样一种功能:你可以先声明这些点的坐标,然后再用数字编号去代替这些点,来表达一个个图形。对于顶点数量庞大的精细模型可以大量节省显存,即便我们顶点数量不多,但用这种方式表达起来也比较清晰。
// 顶点输入 struct Vertex { float x; float y; float z; }; const Vertex vertices[] = { {-1, 1, 0}, {1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, }; D3D11_BUFFER_DESC bd = {}; bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; bd.ByteWidth = sizeof(vertices); bd.StructureByteStride = sizeof(Vertex); D3D11_SUBRESOURCE_DATA sd = {}; sd.pSysMem = vertices; ComPtr<ID3D11Buffer> pVertexBuffer; device->CreateBuffer(&bd, &sd, &pVertexBuffer); UINT stride = sizeof(Vertex); UINT offset = 0u; ID3D11Buffer* vertexBuffers[] = { pVertexBuffer.Get() }; ctx->IASetVertexBuffers(0, 1, vertexBuffers, &stride, &offset);
先声明一个结构体
Vertex,即使我们只准备绘制一个2D图形,但坐标必须得是3D坐标,所以z是必须的,保持为0即可。
vertices变量就是一个
Vertex数组,里面一共四个元素,就是四个顶点的坐标。先调用
ID3D11Device::CreateBuffer创建好顶点数据,然后调用
ID3D11DeviceContext::IASetVertexBuffers把他放进管线。
顶点索引
// 顶点索引 const UINT16 indices[] = { 0,1,2, 0,2,3 }; auto indicesSize = std::size(indices); D3D11_BUFFER_DESC ibd = {}; ibd.BindFlags = D3D11_BIND_INDEX_BUFFER; ibd.ByteWidth = sizeof(indices); ibd.StructureByteStride = sizeof(UINT16); D3D11_SUBRESOURCE_DATA isd = {}; isd.pSysMem = indices; ComPtr<ID3D11Buffer> pIndexBuffer; device->CreateBuffer(&ibd, &isd, &pIndexBuffer); ctx->IASetIndexBuffer(pIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);
indices里面的
0,1,2, 0,2,3就是
vertices数组的索引,千万要注意顺序,dx 绘制三角形是按照顺时针绘制的,如果你把
0,1,2改为
0,2,1,那么这个三角形,就前后反了过来,原本的背面会朝着你,于是因为背面剔除导致你看不见这个三角形了。
我们还需要一个命令告诉dx我们画的是三角形
// 告诉系统我们画的是三角形 ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY::D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
顶点着色器
接下来编写顶点着色器,先添加一个顶点着色器文件,就叫 VertexShader.hlsl 吧。
HLSL全称高级着色器语言,和C++语法当然是不一样的,别担心,我们不需要写很复杂的hlsl代码,特别是顶点着色器,几乎什么也不做,直接原样返回顶点坐标即可:
// VertexShader.hlsl float4 main_VS(float3 pos : POSITION) : SV_POSITION { return float4(pos, 1); }
对着 VertexShader.hlsl 文件右键,点击 属性,调整一些参数:
入口点对应接下来着色器代码的入口函数名,改为
main_VS。因为我们都用 dx11 了,所以着色器模型就选择 Shader Model 5.0 吧。然后是头文件名称改为 VertexShader.h,这样着色器编译后,就会生成一个对应的头文件,在 main.cpp 里直接引入即可。
// 顶点着色器 D3D11_INPUT_ELEMENT_DESC ied[] = { {"POSITION", 0, DXGI_FORMAT::DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0} }; ComPtr<ID3D11InputLayout> pInputLayout; device->CreateInputLayout(ied, std::size(ied), g_main_VS, sizeof(g_main_VS), &pInputLayout); ctx->IASetInputLayout(pInputLayout.Get()); ComPtr<ID3D11VertexShader> pVertexShader; device->CreateVertexShader(g_main_VS, sizeof(g_main_VS), nullptr, &pVertexShader); ctx->VSSetShader(pVertexShader.Get(), 0, 0);
代码中的
g_main_VS就是 VertexShader.h 里的一个变量,代表着色器编译后的内容,由GPU来执行。
创建顶点着色器不难,关键是设置
ID3D11InputLayout的部分。注意到顶点着色器代码入口函数的参数:
float3 pos : POSITION,这个
POSITION可以自己命名,但是要和
D3D11_INPUT_ELEMENT_DESC::SemanticName一致,包括类型 float3 也是和
DXGI_FORMAT_R32G32B32_FLOAT对应的,设置正确的 InputLayout 就是为了和着色器的参数正确对应。
光栅化
光栅化更形象的叫法应该是像素化,根据给定的视点,把3D世界转换为一幅2D图像,并且这个图像的像素数量是有限固定的。
// 光栅化 D3D11_VIEWPORT viewPort = {}; viewPort.TopLeftX = 0; viewPort.TopLeftY = 0; viewPort.Width = 1280; viewPort.Height = 720; viewPort.MaxDepth = 1; viewPort.MinDepth = 0; ctx->RSSetViewports(1, &viewPort);
Width 和 Height 目前和窗口大小相同就行了。
像素着色器
接下来创建一个像素着色器代码文件:PixelShader.hlsl,属性设置和 VertexShader.hlsl 类似,就不重复了。
// PixelShader.hlsl float4 main_PS() : SV_TARGET { float4 pink = float4(1, 0.5, 0.5, 1); // 粉红色 return pink; }
目前我们总是返回一个固定的颜色,粉红色。这里注意格式是固定是RGBA,但每个颜色的范围并不是 0~255,而是 0.0 ~ 1.0。
// 像素着色器 ComPtr<ID3D11PixelShader> pPixelShader; device->CreatePixelShader(g_main_PS, sizeof(g_main_PS), nullptr, &pPixelShader); ctx->PSSetShader(pPixelShader.Get(), 0, 0);
我们不需要对这个像素着色器进行额外的参数输入,所以不需要 InputLayout。
输出合并
输出合并阶段我们把最终的画面写入到后缓冲。
// 输出合并 ComPtr<ID3D11Texture2D> backBuffer; swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backBuffer); CD3D11_RENDER_TARGET_VIEW_DESC renderTargetViewDesc(D3D11_RTV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R8G8B8A8_UNORM); ComPtr<ID3D11RenderTargetView> rtv; device->CreateRenderTargetView(backBuffer.Get(), &renderTargetViewDesc, &rtv); ID3D11RenderTargetView* rtvs[] = { rtv.Get() }; ctx->OMSetRenderTargets(1, rtvs, nullptr);
OMSetRenderTargets不能直接操作
ID3D11Texture2D,需要一个中间层
ID3D11RenderTargetView来实现。把
ID3D11RenderTargetView绑定到后缓冲,然后调用
OMSetRenderTargets把画面往
ID3D11RenderTargetView输出即可。
最终呈现
// Draw Call ctx->DrawIndexed(indicesSize, 0, 0); // 呈现 swapchain->Present(1, 0);
最终调用
DrawIndexed显卡就会开始运算,参数
indicesSize就是顶点数量(6个,包括重复的顶点),调用
Present把画面呈现到窗口中。下面是运行效果:
修改下左上角的顶点:
const Vertex vertices[] = { {-0.5, 0.5, 0}, {1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, };
效果不错
如果你最终运行结果是一片黑,那么可能是哪里搞错了,可以看看输出窗口或者使用VS的图形调试看看:
只有一种颜色看起来太单调了,尝试加个渐变效果把,先修改顶点输入的数据:
// 顶点输入 struct Vertex { float x; float y; float z; struct { float u; float v; } tex; }; const Vertex vertices[] = { {-1, 1, 0, 0, 0}, {1, 1, 0, 1, 0}, {1, -1, 0, 1, 1}, {-1, -1, 0, 0, 1}, }; // ... // 顶点着色器 D3D11_INPUT_ELEMENT_DESC ied[] = { {"POSITION", 0, DXGI_FORMAT::DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0}, {"TEXCOORD", 0, DXGI_FORMAT::DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0} };
注意
vertices现在除了xyz坐标外,还多了两个uv值,u 对应横坐标,v 对应纵坐标,这是用来描述纹理坐标的,待会就来体会他的作用。
然后再修改 VertexShader.hlsl:
// VertexShader.hlsl struct VSOut { float2 tex : TEXCOORD; float4 pos : SV_POSITION; }; VSOut main_VS(float3 pos : POSITION, float2 tex : TEXCOORD) { VSOut vsout; vsout.pos = float4(pos.x, pos.y, pos.z, 1); vsout.tex = tex; return vsout; }
main_VS添加一个新的参数 tex,因此 InputLayout 也要有变化,特别注意 ied 第二个元素的
AlignedByteOffset是上一个元素的字节大小,也就是
DXGI_FORMAT_R32G32B32_FLOAT的字节大小 12 字节。
修改一下 PixelShader.hlsl
// PixelShader.hlsl float4 main_PS(float2 tc : TEXCOORD) : SV_TARGET { float4 color = float4(1, tc.x, tc.y, 1); return color; }
顶点着色器的返回类型现在修改为我们自定义的结构体,返回值除了原来的顶点坐标,还添加了纹理坐标,这样我们在像素着色器中就可以接收到它了。在像素着色器中把绿色和蓝色的值,填入纹理坐标的值,效果如图:
注意四个顶点的对应的纹理坐标参数,左上角 绿色和蓝色 都为0,所以是纯红色,越往右,u值增加,绿色越来越多,和红色混合导致越来越黄。越往下,v值增加,蓝色越来越多,和红色混合导致越来越紫。而右下角是纯白色,因为红绿蓝达到最大值。
纹理采样
现在我们有这样一幅图片,大小 32 x 32,接下来尝试把他当作纹理贴到画面中
首先要解析出图片的RGBA数据,这个我已经做好了(star.h),数据写在一个头文件里面,直接拿来用,就不用再写其他读取图片文件的代码了。
// 纹理创建 ComPtr<ID3D11Texture2D> texture; D3D11_TEXTURE2D_DESC tdesc = {}; tdesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; tdesc.Width = 32; tdesc.Height = 32; tdesc.ArraySize = 1; tdesc.MipLevels = 1; tdesc.SampleDesc = { 1, 0 }; tdesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; D3D11_SUBRESOURCE_DATA tdata = { STAR_RGBA_DATA, 32 * 4, 0}; device->CreateTexture2D(&tdesc, &tdata, &texture);
注意 Format 选择
DXGI_FORMAT_R8G8B8A8_UNORM,Width 和 Height 与图片实际大小保持一致,BindFlags 选择
D3D11_BIND_SHADER_RESOURCE,因为待会着色器需要访问纹理。
// 创建着色器资源视图 D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = CD3D11_SHADER_RESOURCE_VIEW_DESC( texture.Get(), D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R8G8B8A8_UNORM ); ComPtr<ID3D11ShaderResourceView> srv; device->CreateShaderResourceView(texture.Get(), &srvDesc, &srv);
着色器不能直接访问纹理,需要经过一个中间层
ID3D11ShaderResourceView,因此需要创建它。
// 创建采样器 D3D11_SAMPLER_DESC samplerDesc = {}; samplerDesc.Filter = D3D11_FILTER::D3D11_FILTER_ANISOTROPIC; samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP; samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP; samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; samplerDesc.MaxAnisotropy = 16; ComPtr<ID3D11SamplerState> pSampler; device->CreateSamplerState(&samplerDesc, &pSampler);
采样器的作用是根据纹理坐标从纹理中提取像素。例如这个星星图片像素只有 32x32,但是最后却要显示在一个 1280x720 分辨率的四边形中,像素不可能一一对应,而采样器能够生成合适中间过度像素。
D3D11_FILTER_ANISOTROPIC就是各向异性过滤,
MaxAnisotropy是倍数,设置16就行。
// 像素着色器 ComPtr<ID3D11PixelShader> pPixelShader; device->CreatePixelShader(g_main_PS, sizeof(g_main_PS), nullptr, &pPixelShader); ctx->PSSetShader(pPixelShader.Get(), 0, 0);ID3D11ShaderResourceView* srvs[] = { srv.Get() }; ctx->PSSetShaderResources(0, 1, srvs); ID3D11SamplerState* samplers[] = { pSampler.Get() }; ctx->PSSetSamplers(0, 1, samplers);
这里把着色器资源视图和采样器放进管线,接着修改 PixelShader.hlsl:
// PixelShader.hlsl Texture2D<float4> starTexture : t0; SamplerState splr; float4 main_PS(float2 tc : TEXCOORD) : SV_TARGET { float4 color = starTexture.Sample(splr, tc); return color; }
starTexture可以由用户命名,t0 的作用是声明这是第一个纹理,如果有多个纹理就是接着 t1、t2、t3 即可。因为我们只设置了一个采样器,所以直接写
SamplerState splr即可。调用
starTexture.Sample(splr, tc)即可从纹理中取得需要的像素了。
运行效果:
也可以选择不拉伸,而是平铺重复,但这里用不上,我就不一一赘述了。
分离资源创建与渲染过程
Draw函数目前包含了 DirectX 资源的创建操作,比如
CreateTexture2D
CreateBuffer等等,这些操作可以单独提取出来,没有必要每次循环都重新创建这些资源。
void InitScence(ID3D11Device* device, ScenceParam& param) { // 顶点输入 const Vertex vertices[] = { {-1, 1, 0, 0, 0}, {1, 1, 0, 1, 0}, {1, -1, 0, 1, 1}, {-1, -1, 0, 0, 1}, }; D3D11_BUFFER_DESC bd = {}; bd.BindFlags = D3D11_BIND_VERTEX_BUFFER; bd.ByteWidth = sizeof(vertices); bd.StructureByteStride = sizeof(Vertex); D3D11_SUBRESOURCE_DATA sd = {}; sd.pSysMem = vertices; device->CreateBuffer(&bd, &sd, ¶m.pVertexBuffer); D3D11_BUFFER_DESC ibd = {}; ibd.BindFlags = D3D11_BIND_INDEX_BUFFER; ibd.ByteWidth = sizeof(param.indices); ibd.StructureByteStride = sizeof(UINT16); D3D11_SUBRESOURCE_DATA isd = {}; isd.pSysMem = param.indices; device->CreateBuffer(&ibd, &isd, ¶m.pIndexBuffer); // 顶点着色器 D3D11_INPUT_ELEMENT_DESC ied[] = { {"POSITION", 0, DXGI_FORMAT::DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0}, {"TEXCOORD", 0, DXGI_FORMAT::DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0} }; device->CreateInputLayout(ied, std::size(ied), g_main_VS, sizeof(g_main_VS), ¶m.pInputLayout); device->CreateVertexShader(g_main_VS, sizeof(g_main_VS), nullptr, ¶m.pVertexShader); // 纹理创建 D3D11_TEXTURE2D_DESC tdesc = {}; tdesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; tdesc.Width = 32; tdesc.Height = 32; tdesc.ArraySize = 1; tdesc.MipLevels = 1; tdesc.SampleDesc = { 1, 0 }; tdesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; D3D11_SUBRESOURCE_DATA tdata = { STAR_RGBA_DATA, 32 * 4, 0 }; device->CreateTexture2D(&tdesc, &tdata, ¶m.texture); // 创建着色器资源 D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = CD3D11_SHADER_RESOURCE_VIEW_DESC( param.texture.Get(), D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R8G8B8A8_UNORM ); device->CreateShaderResourceView(param.texture.Get(), &srvDesc, ¶m.srv); // 创建采样器 D3D11_SAMPLER_DESC samplerDesc = {}; samplerDesc.Filter = D3D11_FILTER::D3D11_FILTER_ANISOTROPIC; samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP; samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP; samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; samplerDesc.MaxAnisotropy = 16; device->CreateSamplerState(&samplerDesc, ¶m.pSampler); // 像素着色器 device->CreatePixelShader(g_main_PS, sizeof(g_main_PS), nullptr, ¶m.pPixelShader); } void Draw(ID3D11Device* device, ID3D11DeviceContext* ctx, IDXGISwapChain* swapchain, ScenceParam& param) { UINT stride = sizeof(Vertex); UINT offset = 0u; ID3D11Buffer* vertexBuffers[] = { param.pVertexBuffer.Get() }; ctx->IASetVertexBuffers(0, 1, vertexBuffers, &stride, &offset); ctx->IASetIndexBuffer(param.pIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0); ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY::D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); ctx->IASetInputLayout(param.pInputLayout.Get()); ctx->VSSetShader(param.pVertexShader.Get(), 0, 0); // 光栅化 D3D11_VIEWPORT viewPort = {}; viewPort.TopLeftX = 0; viewPort.TopLeftY = 0; viewPort.Width = 1280; viewPort.Height = 720; viewPort.MaxDepth = 1; viewPort.MinDepth = 0; ctx->RSSetViewports(1, &viewPort); ctx->PSSetShader(param.pPixelShader.Get(), 0, 0); ID3D11ShaderResourceView* srvs[] = { param.srv.Get() }; ctx->PSSetShaderResources(0, 1, srvs); ID3D11SamplerState* samplers[] = { param.pSampler.Get() }; ctx->PSSetSamplers(0, 1, samplers); // 输出合并 ComPtr<ID3D11Texture2D> backBuffer; swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&backBuffer); CD3D11_RENDER_TARGET_VIEW_DESC renderTargetViewDesc(D3D11_RTV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R8G8B8A8_UNORM); ComPtr<ID3D11RenderTargetView> rtv; device->CreateRenderTargetView(backBuffer.Get(), &renderTargetViewDesc, &rtv); ID3D11RenderTargetView* rtvs[] = { rtv.Get() }; ctx->OMSetRenderTargets(1, rtvs, nullptr); // Draw Call auto indicesSize = std::size(param.indices); ctx->DrawIndexed(indicesSize, 0, 0); // 呈现 swapchain->Present(1, 0); }
InitScence负责创建 DirectX 资源,
Draw仅负责执行渲染指令。
再稍微修改 main 函数:
// ... D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, NULL, NULL, D3D11_SDK_VERSION, &swapChainDesc, &swapChain, &d3ddeivce, NULL, &d3ddeviceCtx); ScenceParam scenceParam; InitScence(d3ddeivce.Get(), scenceParam); auto currentTime = system_clock::now(); MSG msg; while (1) { // ... if (hasMsg) { // ... } else { Draw(d3ddeivce.Get(), d3ddeviceCtx.Get(), swapChain.Get(), scenceParam); } } // ...
D3D11VA 硬件解码
好了,最困难的部分已经过去,终于可以回到 FFmpeg 的部分了。之前硬件解码使用的设备类型是
AV_HWDEVICE_TYPE_DXVA2,这回换成
AV_HWDEVICE_TYPE_D3D11VA:
// 启用硬件解码器 AVBufferRef* hw_device_ctx = nullptr; av_hwdevice_ctx_create(&hw_device_ctx, AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA, NULL, NULL, NULL); vcodecCtx->hw_device_ctx = hw_device_ctx;
观察解码出来的
AVFrame::format,是
AV_PIX_FMT_D3D11,依旧看看他的注释:
data[0] 是一个
ID3D11Texture2D,这就是为什么前面要大费周章讲这么多,为的就是说明纹理如何最终显示在屏幕上。注释还提到了 data[1] 是纹理数组的索引,事实上
ID3D11Texture2D可以存储多个纹理,待会我们把 data[0] 的纹理复制出来的时候就要用到这个索引值。
现在的问题是,不同的 d3d11device 之间的
ID3D11Texture2D,是没法直接访问的,因此需要做一些操作实现纹理共享。
struct ScenceParam { // ... ComPtr<ID3D11Texture2D> texture; HANDLE sharedHandle; ComPtr<ID3D11ShaderResourceView> srvY; ComPtr<ID3D11ShaderResourceView> srvUV; // ... };
在
ScenceParam结构体添加一个
HANDLE sharedHandle,存储共享句柄。再添加两个着色器资源视图:srvY 和 srvUV。
void InitScence(ID3D11Device* device, ScenceParam& param, const DecoderParam& decoderParam) { // ... // 纹理创建 D3D11_TEXTURE2D_DESC tdesc = {}; tdesc.Format = DXGI_FORMAT_NV12; tdesc.Usage = D3D11_USAGE_DEFAULT; tdesc.MiscFlags = D3D11_RESOURCE_MISC_SHARED; tdesc.ArraySize = 1; tdesc.MipLevels = 1; tdesc.SampleDesc = { 1, 0 }; tdesc.Height = decoderParam.height; tdesc.Width = decoderParam.width; tdesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; device->CreateTexture2D(&tdesc, nullptr, ¶m.texture); // 创建纹理共享句柄 ComPtr<IDXGIResource> dxgiShareTexture; param.texture->QueryInterface(__uuidof(IDXGIResource), (void**)dxgiShareTexture.GetAddressOf()); dxgiShareTexture->GetSharedHandle(¶m.sharedHandle); // 创建着色器资源 D3D11_SHADER_RESOURCE_VIEW_DESC const YPlaneDesc = CD3D11_SHADER_RESOURCE_VIEW_DESC( param.texture.Get(), D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R8_UNORM ); device->CreateShaderResourceView( param.texture.Get(), &YPlaneDesc, ¶m.srvY ); D3D11_SHADER_RESOURCE_VIEW_DESC const UVPlaneDesc = CD3D11_SHADER_RESOURCE_VIEW_DESC( param.texture.Get(), D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R8G8_UNORM ); device->CreateShaderResourceView( param.texture.Get(), &UVPlaneDesc, ¶m.srvUV ); // ... }
创建纹理的时候,Format 注意选择
DXGI_FORMAT_NV12,和 FFmpeg 解码出来的纹理一致。
MiscFlags设置为
D3D11_RESOURCE_MISC_SHARED,这样这个纹理才能共享出去。调用
IDXGIResource::GetSharedHandle可以获得一个句柄,拿着这个句柄,待会就可以用 FFmpeg 的 d3d 设备操作这个纹理了。
根据微软官方的文档描述 DXGI_FORMAT,
DXGI_FORMAT_NV12纹理格式应当使用两个着色器资源视图去处理,一个视图的格式是 DXGI_FORMAT_R8_UNORM,对应Y通道,一个视图的格式是 DXGI_FORMAT_R8G8_UNORM,对应UV通道,所以这里需要创建两个着色器资源视图。后面调用
PSSetShaderResources时,把两个视图都放进管线:
void Draw(ID3D11Device* device, ID3D11DeviceContext* ctx, IDXGISwapChain* swapchain, ScenceParam& param) { // ... ID3D11ShaderResourceView* srvs[] = { param.srvY.Get(), param.srvUV.Get() }; ctx->PSSetShaderResources(0, std::size(srvs), srvs); // ... }
编写一个新函数
UpdateVideoTexture把 FFmpeg 解码出来的纹理复制到我们自己创建的纹理中:
void UpdateVideoTexture(AVFrame* frame, const ScenceParam& scenceParam, const DecoderParam& decoderParam) { ID3D11Texture2D* t_frame = (ID3D11Texture2D*)frame->data[0]; int t_index = (int)frame->data[1]; ComPtr<ID3D11Device> device; t_frame->GetDevice(device.GetAddressOf()); ComPtr<ID3D11DeviceContext> deviceCtx; device->GetImmediateContext(&deviceCtx); ComPtr<ID3D11Texture2D> videoTexture; device->OpenSharedResource(scenceParam.sharedHandle, __uuidof(ID3D11Texture2D), (void**)&videoTexture); deviceCtx->CopySubresourceRegion(videoTexture.Get(), 0, 0, 0, 0, t_frame, t_index, 0); deviceCtx->Flush(); }
ID3D11Device::OpenSharedResource可以通过刚刚创建的共享句柄打开由我们创建的纹理,再调用
CopySubresourceRegion把 FFmpeg 的纹理复制过来。最后注意必须要调用
Flush,强制 GPU 清空当前命令缓冲区,否则可能会出现画面一闪一闪,看到绿色帧的问题(不一定每台电脑都可能发生)。
最后修改 main 函数
int WINAPI WinMain ( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd ) { // ... DecoderParam decoderParam; ScenceParam scenceParam; InitDecoder(filePath.c_str(), decoderParam); // ... InitScence(d3ddeivce.Get(), scenceParam, decoderParam); // ... MSG msg; while (1) { // ... if (hasMsg) { // ... } else { auto frame = RequestFrame(decoderParam); UpdateVideoTexture(frame, scenceParam, decoderParam); Draw(d3ddeivce.Get(), d3ddeviceCtx.Get(), swapChain.Get(), scenceParam); av_frame_free(&frame); } } }
运行结果:
能看到画面,但是全是红色,非常瘆人。
原因是我们没有正确修改 PixelShader.hlsl,现在第一个着色器资源不再是
Texture2D<float4>类型了,而应该是
Texture2D<float>,就是Y通道。此时程序运行并不会出现错误提示,而是会进行一个类型转换,直接把
float转换成
float4,比如
float(1)会变成
float4(1, 0, 0, 0),导致Y通道的数值落在了红色上(RGBA,R是第一个),因此我们看到的画面就只有红色了。下面修改为正确的代码:
// PixelShader.hlsl Texture2D<float> yChannel : t0; Texture2D<float2> uvChannel : t1; SamplerState splr; static const float3x3 YUVtoRGBCoeffMatrix = { 1.164383f, 1.164383f, 1.164383f, 0.000000f, -0.391762f, 2.017232f, 1.596027f, -0.812968f, 0.000000f }; float3 ConvertYUVtoRGB(float3 yuv) { // Derived from https://msdn.microsoft.com/en-us/library/windows/desktop/dd206750(v=vs.85).aspx // Section: Converting 8-bit YUV to RGB888 // These values are calculated from (16 / 255) and (128 / 255) yuv -= float3(0.062745f, 0.501960f, 0.501960f); yuv = mul(yuv, YUVtoRGBCoeffMatrix); return saturate(yuv); } float4 main_PS(float2 tc : TEXCOORD) : SV_TARGET { float y = yChannel.Sample(splr, tc); float2 uv = uvChannel.Sample(splr, tc); float3 rgb = ConvertYUVtoRGB(float3(y, uv)); return float4(rgb, 1); }
看起来我们有两个纹理:
yChannel和
uvChannel,但其实只是对同一个纹理的两种读取方式而已。还记得前面提到的 YUV420P 的采样方式吗,4个Y共用一个UV,这里采样器非常巧妙的完成了这项工作,根据纹理坐标提取了合适的数值。最后
ConvertYUVtoRGB函数把 yuv 转换为 rgb 值(这个是我在网上抄的)。
最终运行结果:
完美!
很遗憾,目前为止还是没能讲完播放器所有的内容,因为dx11实在太复杂了,直接花了一整篇讲,争取下一篇讲完所有内容。
- 如何用FFmpeg+Win32实现一个简单的音频播放器
- vba使用win32 API(GetOpenFileName )实现打开文件对话框
- 面向对象的Windows编程实战(上)(使用C++和Win32 API)
- 对于一个频繁使用的短小函数,在C语言中应用什么实现,在C++中应用什么实现?
- vba使用win32 API(GetOpenFileName )实现打开文件对话框
- 从零开始学C++之重载 operator new 和 operator delete 实现一个简单内存泄漏跟踪器
- C/C++系列之如何实现一个avi格式的播放器
- 学习实践:使用模式,原则实现一个C++数据库訪问类
- vba使用win32 API(GetOpenFileName )实现打开文件对话框
- 如何使用win32 API实现将文件放入回收站
- 使用ffmpeg实现合并多个音频为一个音频的方法
- 在win32 api程序中:使用C++风格的字符串
- 使用python的Flask实现一个RESTful API服务器端[翻译]
- 使用FFMPEG实现音频播放器
- 使用WIN32汇编语言实现一个基本windows窗体的过程分析
- 使用python的Flask实现一个RESTful API服务器端[翻译]
- vba使用win32 API(GetOpenFileName )实现打开文件对话框
- vba使用win32 API(GetOpenFileName )实现打开文件对话框
- 对于一个频繁使用的短小函数,在C语言中应用什么实现,在C++中应用什么实现?
- 从零开始学C++之重载 operator new 和 operator delete 实现一个简单内存泄漏跟踪器