• 【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 之后就会看到输出显示:

    image

    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 的各个枚举值

    image

    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 图形管线有很多阶段,但我们不需要每一阶段都用上,以下就是我们必须编程的阶段:

    1. Input-Assembler Stage(输入装配)
    2. Vertex Shader Stage (顶点着色器)
    3. Rasterizer Stage (光栅化)
    4. Pixel Shader Stage (像素着色器)
    5. Output-Merger Stage (输出合并)

    完整的管线阶段看这个图(不看也行):

    image

    GPU需要经历若干个阶段才能最终熬制1帧画面,每一个阶段都需要上一个阶段的运行结果作为参数输入,同时也可能需要额外加入新的输入参数。

    我们新增一个函数 Draw 来实现上面必经阶段:

    void Draw(ID3D11Device* device, ID3D11DeviceContext* ctx, IDXGISwapChain* swapchain) {
    	// 顶点输入
    	// ...
    
    	// 顶点索引
    	// ..
    
    	// 顶点着色器
    	// ...
    
    	// 光栅化
    	// ...
    
    	// 像素着色器
    	// ...
    
    	// 输出合并
    	// ...
    
    	// Draw Call
    	// ...
    
    	// 呈现
    	// ...
    }
    

    顶点输入

    一个四边形有4个顶点,假设是一个边长为 2 的正方形,中心点坐标是(0,0),那么四个角的坐标很容易就可以得出,如图所示:

    image

    但是 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,那么这个三角形,就前后反了过来,原本的背面会朝着你,于是因为背面剔除导致你看不见这个三角形了。

    image

    我们还需要一个命令告诉dx我们画的是三角形

    // 告诉系统我们画的是三角形
    ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY::D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    

    顶点着色器

    接下来编写顶点着色器,先添加一个顶点着色器文件,就叫 VertexShader.hlsl 吧。

    image

    HLSL全称高级着色器语言,和C++语法当然是不一样的,别担心,我们不需要写很复杂的hlsl代码,特别是顶点着色器,几乎什么也不做,直接原样返回顶点坐标即可:

    // VertexShader.hlsl
    float4 main_VS(float3 pos : POSITION) : SV_POSITION
    {
        return float4(pos, 1);
    }
    

    对着 VertexShader.hlsl 文件右键,点击 属性,调整一些参数:

    image

    image

    入口点对应接下来着色器代码的入口函数名,改为 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 把画面呈现到窗口中。下面是运行效果:

    image

    修改下左上角的顶点:

    const Vertex vertices[] = {
    	{-0.5,	0.5,	0},
    	{1,		1,	0},
    	{1,		-1,	0},
    	{-1,	-1,	0},
    };
    

    image

    效果不错

    如果你最终运行结果是一片黑,那么可能是哪里搞错了,可以看看输出窗口或者使用VS的图形调试看看:

    image

    image

    只有一种颜色看起来太单调了,尝试加个渐变效果把,先修改顶点输入的数据:

    // 顶点输入
    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;
    }
    

    顶点着色器的返回类型现在修改为我们自定义的结构体,返回值除了原来的顶点坐标,还添加了纹理坐标,这样我们在像素着色器中就可以接收到它了。在像素着色器中把绿色和蓝色的值,填入纹理坐标的值,效果如图:

    image

    注意四个顶点的对应的纹理坐标参数,左上角 绿色和蓝色 都为0,所以是纯红色,越往右,u值增加,绿色越来越多,和红色混合导致越来越黄。越往下,v值增加,蓝色越来越多,和红色混合导致越来越紫。而右下角是纯白色,因为红绿蓝达到最大值。

    纹理采样

    现在我们有这样一幅图片,大小 32 x 32,接下来尝试把他当作纹理贴到画面中

    image

    首先要解析出图片的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) 即可从纹理中取得需要的像素了。

    运行效果:

    image

    也可以选择不拉伸,而是平铺重复,但这里用不上,我就不一一赘述了。

    分离资源创建与渲染过程

    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, &param.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, &param.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), &param.pInputLayout);
    	device->CreateVertexShader(g_main_VS, sizeof(g_main_VS), nullptr, &param.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, &param.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, &param.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, &param.pSampler);
    
    	// 像素着色器
    	device->CreatePixelShader(g_main_PS, sizeof(g_main_PS), nullptr, &param.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,依旧看看他的注释:

    image

    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, &param.texture);
    
    	// 创建纹理共享句柄
    	ComPtr<IDXGIResource> dxgiShareTexture;
    	param.texture->QueryInterface(__uuidof(IDXGIResource), (void**)dxgiShareTexture.GetAddressOf());
    	dxgiShareTexture->GetSharedHandle(&param.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,
    		&param.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,
    		&param.srvUV
    	);
    	// ...
    }
    

    创建纹理的时候,Format 注意选择 DXGI_FORMAT_NV12,和 FFmpeg 解码出来的纹理一致。MiscFlags 设置为 D3D11_RESOURCE_MISC_SHARED,这样这个纹理才能共享出去。调用 IDXGIResource::GetSharedHandle 可以获得一个句柄,拿着这个句柄,待会就可以用 FFmpeg 的 d3d 设备操作这个纹理了。

    根据微软官方的文档描述 DXGI_FORMATDXGI_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);
    		}
    	}
    }
    

    运行结果:

    image

    能看到画面,但是全是红色,非常瘆人。

    原因是我们没有正确修改 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);
    }
    

    看起来我们有两个纹理:yChanneluvChannel,但其实只是对同一个纹理的两种读取方式而已。还记得前面提到的 YUV420P 的采样方式吗,4个Y共用一个UV,这里采样器非常巧妙的完成了这项工作,根据纹理坐标提取了合适的数值。最后 ConvertYUVtoRGB 函数把 yuv 转换为 rgb 值(这个是我在网上抄的)。

    最终运行结果:

    image

    完美!

    很遗憾,目前为止还是没能讲完播放器所有的内容,因为dx11实在太复杂了,直接花了一整篇讲,争取下一篇讲完所有内容。

  • 相关阅读:
    Ant-编译构建(2)-第3方jar包引入、log4j2
    Ant-编译构建(1)-HelloWorld
    java List的初始化
    传入json字符串的post请求
    HttPclient 以post方式发送json
    cron表达式详解,cron表达式写法,cron表达式例子
    深入理解SQL的四种连接-左外连接、右外连接、内连接、全连接
    Java两种延时——thread和timer
    List<List<Object>> list = new ArrayList<List<Object>>(); 求回答补充问题 list.get(position).add(Object);为什么会报错啊我想在对应的list里面添加对象
    关于 charset 的几种编码方式
  • 原文地址:https://www.cnblogs.com/judgeou/p/14728617.html
Copyright © 2020-2023  润新知