前情提要
前篇: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实在太复杂了,直接花了一整篇讲,争取下一篇讲完所有内容。