UE4 使用 ID3D11Texture2D 捕获帧并转换为 R8G8B8 位图
UE4 capture frame using ID3D11Texture2D and convert to R8G8B8 bitmap
我正在使用 UE4 开发流媒体原型。
我的目标(在此 post)完全是关于 捕获帧并将其中一个保存为位图 ,只是为了在视觉上确保帧被正确捕获。
我目前正在捕获帧,将后备缓冲区转换为 ID3D11Texture2D,然后对其进行映射。
注意 :我在渲染线程中尝试了 ReadSurfaceData 方法,但它在性能方面表现不佳 (FPS 去了降到 15,我想以 60 FPS 的速度捕捉),而来自后缓冲区的 DirectX 纹理映射目前需要 1 到 3 毫秒。
调试时,我可以看到 D3D11_TEXTURE2D_DESC's format is DXGI_FORMAT_R10G10B10A2_UNORM,因此 red/green/blues 分别存储在 10 位上,alpha 存储在 2 位上。
我的问题:
- 如何将纹理数据(使用 D3D11_MAPPED_SUBRESOURCE pData 指针)转换为 R8G8B8(A8),即每种颜色 8 位 (没有 alpha 的 R8G8B8 也可以对我来说很好) ?
- 此外,我在捕获帧方面做错了什么吗?
我试过的:
以下所有代码都在注册到 OnBackBufferReadyToPresent(下面的代码)的回调函数中执行。
void* NativeResource = BackBuffer->GetNativeResource();
if (NativeResource == nullptr)
{
UE_LOG(LogTemp, Error, TEXT("Couldn't retrieve native resource"));
return;
}
ID3D11Texture2D* BackBufferTexture = static_cast<ID3D11Texture2D*>(NativeResource);
D3D11_TEXTURE2D_DESC BackBufferTextureDesc;
BackBufferTexture->GetDesc(&BackBufferTextureDesc);
// Get the device context
ID3D11Device* d3dDevice;
BackBufferTexture->GetDevice(&d3dDevice);
ID3D11DeviceContext* d3dContext;
d3dDevice->GetImmediateContext(&d3dContext);
// Staging resource
ID3D11Texture2D* StagingTexture;
D3D11_TEXTURE2D_DESC StagingTextureDesc = BackBufferTextureDesc;
StagingTextureDesc.Usage = D3D11_USAGE_STAGING;
StagingTextureDesc.BindFlags = 0;
StagingTextureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
StagingTextureDesc.MiscFlags = 0;
HRESULT hr = d3dDevice->CreateTexture2D(&StagingTextureDesc, nullptr, &StagingTexture);
if (FAILED(hr))
{
UE_LOG(LogTemp, Error, TEXT("CreateTexture failed"));
}
// Copy the texture to the staging resource
d3dContext->CopyResource(StagingTexture, BackBufferTexture);
// Map the staging resource
D3D11_MAPPED_SUBRESOURCE mapInfo;
hr = d3dContext->Map(
StagingTexture,
0,
D3D11_MAP_READ,
0,
&mapInfo);
if (FAILED(hr))
{
UE_LOG(LogTemp, Error, TEXT("Map failed"));
}
// See https://dev.to/muiz6/c-how-to-write-a-bitmap-image-from-scratch-1k6m for the struct definitions & the initialization of bmpHeader and bmpInfoHeader
// I didn't copy that code here to avoid overloading this post, as it's identical to the article's code
// Just making clear the reassigned values below
bmpHeader.sizeOfBitmapFile = 54 + StagingTextureDesc.Width * StagingTextureDesc.Height * 4;
bmpInfoHeader.width = StagingTextureDesc.Width;
bmpInfoHeader.height = StagingTextureDesc.Height;
std::ofstream fout("output.bmp", std::ios::binary);
fout.write((char*)&bmpHeader, 14);
fout.write((char*)&bmpInfoHeader, 40);
// TODO : convert to R8G8B8 (see below for my attempt at this)
fout.close();
StagingTexture->Release();
d3dContext->Unmap(StagingTexture, 0);
d3dContext->Release();
d3dDevice->Release();
BackBufferTexture->Release();
(如代码注释中所述,我按照 this article about the BMP headers 将位图保存到文件)
纹理数据
我担心的一件事是使用此方法检索的数据。
我使用了一个临时数组来检查调试器里面有什么。
// Just noted which width and height had the texture and hardcoded it here to allocate the right size
uint32_t data[1936 * 1056];
// Multiply by 4 as there are 4 bytes (32 bits) per pixel
memcpy(data, mapInfo.pData, StagingTextureDesc.Width * StagingTextureDesc.Height * 4);
原来这个数组中的第一个 1935 uint32 都包含相同的值; 3595933029
。之后,相同的值经常连续出现数百次。
这让我觉得帧没有按应有的方式捕获,因为 UE4 编辑器的 window 的第一行始终没有完全相同的颜色(无论是顶部还是底部)。
R10G10B10A2 至 R8G8B8(A8)
所以我试着猜测如何从R10G10B10A2转换为R8G8B8。我从数据缓冲区开头连续出现 1935 次的这个值开始:3595933029
.
当我选择编辑器的 window 屏幕截图颜色时(使用 Windows 工具,它为我提供了与 DirectX 纹理尺寸完全相同的图像,即 1936x1056),我得到了以下不同颜色:
- R=56, G=57, B=52(左上&左下)
- R=0, G=0, B=0(右上角)
- R=46, G=40, B=72(右下 - 它与任务栏重叠,因此颜色)
所以我尝试手动转换颜色以检查它是否与我选择的颜色匹配。
我想过位移来简单地比较值
3595933029
(检索缓冲区中的值)二进制:11010110010101011001010101100101
- 已经可以看到模式:
11
后跟3次10位值0101100101
,然后是none所拾取的颜色(黑色角除外,它虽然只会由零组成)
- 无论如何,假设
RRRRRRRRRR GGGGGGGGGG BBBBBBBBBB AA
顺序(丢弃的位用 x 标记):
11010110xx01010110xx01010110xxxx
- R=214, G=86, B=86 : 不匹配
- 假设
AA RRRRRRRRRR GGGGGGGGGG BBBBBBBBBB
:
xx01011001xx01011001xx01011001xx
- R=89,G=89,B=89:不匹配
如果这有帮助,这里是应该捕获的编辑器 window(它确实是一个第三人称模板,除了这个捕获代码之外没有添加任何东西)
这是移位时生成的位图:
生成位图像素数据的代码:
struct Pixel {
uint8_t blue = 0;
uint8_t green = 0;
uint8_t red = 0;
} pixel;
uint32_t* pointer = (uint32_t*)mapInfo.pData;
size_t numberOfPixels = bmpInfoHeader.width * bmpInfoHeader.height;
for (int i = 0; i < numberOfPixels; i++) {
uint32_t value = *pointer;
// Ditch the color's 2 last bits, keep the 8 first
pixel.blue = value >> 2;
pixel.green = value >> 12;
pixel.red = value >> 22;
++pointer;
fout.write((char*)&pixel, 3);
}
现在的颜色看起来有点像,但是一点都不像小编
我错过了什么?
首先,您假设 mapInfo.RowPitch
恰好是 StagicngTextureDesc.Width * 4
。这通常不是真的。复制 to/from Direct3D 资源时,需要进行 'row-by-row' 次复制。此外,在堆栈上分配 2 MBytes 也不是好的做法。
#include <cstdint>
#include <memory>
// Assumes our staging texture is 4 bytes-per-pixel
// Allocate temporary memory
auto data = std::unique_ptr<uint32_t[]>(
new uint32_t[StagingTextureDesc.Width * StagingTextureDesc.Height]);
auto src = static_cast<uint8_t*>(mapInfo.pData);
uint32_t* dest = data.get();
for(UINT y = 0; y < StagingTextureDesc.Height; ++y)
{
// Multiply by 4 as there are 4 bytes (32 bits) per pixel
memcpy(dest, src, StagingTextureDesc.Width * sizeof(uint32_t));
src += mapInfo.RowPitch;
dest += StagingTextureDesc.Width;
}
For C++11, using std::unique_ptr
ensures the memory is eventually released automatically. You can transfer ownership of the memory to something else with uint32_t* ptr = data.release()
. See cppreference.
With C++14, the better way to write the allocation is: auto data = std::make_unique<uint32_t[]>(StagingTextureDesc.Width * StagingTextureDesc.Height);
. This assumes you are fine with a C++ exception being thrown for out-of-memory.
If you want to return an error code for out-of-memory instead of a C++ exception, use: auto data = std::unique_ptr<uint32_t[]>(new (std::nothrow) uint32_t[StagingTextureDesc.Width * StagingTextureDesc.Height]); if (!data) // return error
将 10:10:10:2 内容转换为 8:8:8:8 内容可以在 CPU 上通过位移有效地完成。
棘手的一点是处理将 2 位 alpha 放大到 8 位。例如,您希望 11
的 Alpha 映射到 255,而不是 192。
这是上面循环的替换
// Assumes our staging texture is DXGI_FORMAT_R10G10B10A2_UNORM
for(UINT y = 0; y < StagingTextureDesc.Height; ++y)
{
auto sptr = reinterpret_cast<uint32_t*>(src);
for(UINT x = 0; x < StagingTextureDesc.Width; ++x)
{
uint32_t t = *(sptr++);
uint32_t r = (t & 0x000003ff) >> 2;
uint32_t g = (t & 0x000ffc00) >> 12;
uint32_t b = (t & 0x3ff00000) >> 22;
// Upscale alpha
// 11xxxxxx -> 11111111 (255)
// 10xxxxxx -> 10101010 (170)
// 01xxxxxx -> 01010101 (85)
// 00xxxxxx -> 00000000 (0)
t &= 0xc0000000;
uint32_t a = (t >> 24) | (t >> 26) | (t >> 28) | (t >> 30);
// Convert to DXGI_FORMAT_R8G8B8A8_UNORM
*(dest++) = r | (g << 8) | (b << 16) | (a << 24);
}
src += mapInfo.RowPitch;
}
当然我们可以组合移位操作,因为我们将它们向下移动然后在上一个循环中返回。我们确实需要更新掩码以删除通常被全班移走的位。这将替换上面循环的内部主体:
// Convert from 10:10:10:2 to 8:8:8:8
uint32_t t = *(sptr++);
uint32_t r = (t & 0x000003fc) >> 2;
uint32_t g = (t & 0x000ff000) >> 4;
uint32_t b = (t & 0x3fc00000) >> 6;
t &= 0xc0000000;
uint32_t a = t | (t >> 2) | (t >> 4) | (t >> 6);
*(dest++) = r | g | b | a;
任何时候减少位深度都会引入错误。 ordered dithering and error-diffusion dithering 等技术通常用于这种性质的像素转换。这些给图像引入了一点噪声,以减少丢失的低位的视觉影响。
For examples of conversions for all DXGI_FORMAT
types, see DirectXTex which makes use of DirectXMath for all the various packed vector types. DirectXTex also implements both 4x4 ordered dithering and Floyd-Steinberg error-diffusion dithering when reducing bit-depth.
我正在使用 UE4 开发流媒体原型。 我的目标(在此 post)完全是关于 捕获帧并将其中一个保存为位图 ,只是为了在视觉上确保帧被正确捕获。
我目前正在捕获帧,将后备缓冲区转换为 ID3D11Texture2D,然后对其进行映射。
注意 :我在渲染线程中尝试了 ReadSurfaceData 方法,但它在性能方面表现不佳 (FPS 去了降到 15,我想以 60 FPS 的速度捕捉),而来自后缓冲区的 DirectX 纹理映射目前需要 1 到 3 毫秒。
调试时,我可以看到 D3D11_TEXTURE2D_DESC's format is DXGI_FORMAT_R10G10B10A2_UNORM,因此 red/green/blues 分别存储在 10 位上,alpha 存储在 2 位上。
我的问题:
- 如何将纹理数据(使用 D3D11_MAPPED_SUBRESOURCE pData 指针)转换为 R8G8B8(A8),即每种颜色 8 位 (没有 alpha 的 R8G8B8 也可以对我来说很好) ?
- 此外,我在捕获帧方面做错了什么吗?
我试过的:
以下所有代码都在注册到 OnBackBufferReadyToPresent(下面的代码)的回调函数中执行。
void* NativeResource = BackBuffer->GetNativeResource();
if (NativeResource == nullptr)
{
UE_LOG(LogTemp, Error, TEXT("Couldn't retrieve native resource"));
return;
}
ID3D11Texture2D* BackBufferTexture = static_cast<ID3D11Texture2D*>(NativeResource);
D3D11_TEXTURE2D_DESC BackBufferTextureDesc;
BackBufferTexture->GetDesc(&BackBufferTextureDesc);
// Get the device context
ID3D11Device* d3dDevice;
BackBufferTexture->GetDevice(&d3dDevice);
ID3D11DeviceContext* d3dContext;
d3dDevice->GetImmediateContext(&d3dContext);
// Staging resource
ID3D11Texture2D* StagingTexture;
D3D11_TEXTURE2D_DESC StagingTextureDesc = BackBufferTextureDesc;
StagingTextureDesc.Usage = D3D11_USAGE_STAGING;
StagingTextureDesc.BindFlags = 0;
StagingTextureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
StagingTextureDesc.MiscFlags = 0;
HRESULT hr = d3dDevice->CreateTexture2D(&StagingTextureDesc, nullptr, &StagingTexture);
if (FAILED(hr))
{
UE_LOG(LogTemp, Error, TEXT("CreateTexture failed"));
}
// Copy the texture to the staging resource
d3dContext->CopyResource(StagingTexture, BackBufferTexture);
// Map the staging resource
D3D11_MAPPED_SUBRESOURCE mapInfo;
hr = d3dContext->Map(
StagingTexture,
0,
D3D11_MAP_READ,
0,
&mapInfo);
if (FAILED(hr))
{
UE_LOG(LogTemp, Error, TEXT("Map failed"));
}
// See https://dev.to/muiz6/c-how-to-write-a-bitmap-image-from-scratch-1k6m for the struct definitions & the initialization of bmpHeader and bmpInfoHeader
// I didn't copy that code here to avoid overloading this post, as it's identical to the article's code
// Just making clear the reassigned values below
bmpHeader.sizeOfBitmapFile = 54 + StagingTextureDesc.Width * StagingTextureDesc.Height * 4;
bmpInfoHeader.width = StagingTextureDesc.Width;
bmpInfoHeader.height = StagingTextureDesc.Height;
std::ofstream fout("output.bmp", std::ios::binary);
fout.write((char*)&bmpHeader, 14);
fout.write((char*)&bmpInfoHeader, 40);
// TODO : convert to R8G8B8 (see below for my attempt at this)
fout.close();
StagingTexture->Release();
d3dContext->Unmap(StagingTexture, 0);
d3dContext->Release();
d3dDevice->Release();
BackBufferTexture->Release();
(如代码注释中所述,我按照 this article about the BMP headers 将位图保存到文件)
纹理数据
我担心的一件事是使用此方法检索的数据。 我使用了一个临时数组来检查调试器里面有什么。
// Just noted which width and height had the texture and hardcoded it here to allocate the right size
uint32_t data[1936 * 1056];
// Multiply by 4 as there are 4 bytes (32 bits) per pixel
memcpy(data, mapInfo.pData, StagingTextureDesc.Width * StagingTextureDesc.Height * 4);
原来这个数组中的第一个 1935 uint32 都包含相同的值; 3595933029
。之后,相同的值经常连续出现数百次。
这让我觉得帧没有按应有的方式捕获,因为 UE4 编辑器的 window 的第一行始终没有完全相同的颜色(无论是顶部还是底部)。
R10G10B10A2 至 R8G8B8(A8)
所以我试着猜测如何从R10G10B10A2转换为R8G8B8。我从数据缓冲区开头连续出现 1935 次的这个值开始:3595933029
.
当我选择编辑器的 window 屏幕截图颜色时(使用 Windows 工具,它为我提供了与 DirectX 纹理尺寸完全相同的图像,即 1936x1056),我得到了以下不同颜色:
- R=56, G=57, B=52(左上&左下)
- R=0, G=0, B=0(右上角)
- R=46, G=40, B=72(右下 - 它与任务栏重叠,因此颜色)
所以我尝试手动转换颜色以检查它是否与我选择的颜色匹配。 我想过位移来简单地比较值
3595933029
(检索缓冲区中的值)二进制:11010110010101011001010101100101
- 已经可以看到模式:
11
后跟3次10位值0101100101
,然后是none所拾取的颜色(黑色角除外,它虽然只会由零组成)
- 已经可以看到模式:
- 无论如何,假设
RRRRRRRRRR GGGGGGGGGG BBBBBBBBBB AA
顺序(丢弃的位用 x 标记):11010110xx01010110xx01010110xxxx
- R=214, G=86, B=86 : 不匹配
- 假设
AA RRRRRRRRRR GGGGGGGGGG BBBBBBBBBB
:xx01011001xx01011001xx01011001xx
- R=89,G=89,B=89:不匹配
如果这有帮助,这里是应该捕获的编辑器 window(它确实是一个第三人称模板,除了这个捕获代码之外没有添加任何东西)
struct Pixel {
uint8_t blue = 0;
uint8_t green = 0;
uint8_t red = 0;
} pixel;
uint32_t* pointer = (uint32_t*)mapInfo.pData;
size_t numberOfPixels = bmpInfoHeader.width * bmpInfoHeader.height;
for (int i = 0; i < numberOfPixels; i++) {
uint32_t value = *pointer;
// Ditch the color's 2 last bits, keep the 8 first
pixel.blue = value >> 2;
pixel.green = value >> 12;
pixel.red = value >> 22;
++pointer;
fout.write((char*)&pixel, 3);
}
现在的颜色看起来有点像,但是一点都不像小编
我错过了什么?
首先,您假设 mapInfo.RowPitch
恰好是 StagicngTextureDesc.Width * 4
。这通常不是真的。复制 to/from Direct3D 资源时,需要进行 'row-by-row' 次复制。此外,在堆栈上分配 2 MBytes 也不是好的做法。
#include <cstdint>
#include <memory>
// Assumes our staging texture is 4 bytes-per-pixel
// Allocate temporary memory
auto data = std::unique_ptr<uint32_t[]>(
new uint32_t[StagingTextureDesc.Width * StagingTextureDesc.Height]);
auto src = static_cast<uint8_t*>(mapInfo.pData);
uint32_t* dest = data.get();
for(UINT y = 0; y < StagingTextureDesc.Height; ++y)
{
// Multiply by 4 as there are 4 bytes (32 bits) per pixel
memcpy(dest, src, StagingTextureDesc.Width * sizeof(uint32_t));
src += mapInfo.RowPitch;
dest += StagingTextureDesc.Width;
}
For C++11, using
std::unique_ptr
ensures the memory is eventually released automatically. You can transfer ownership of the memory to something else withuint32_t* ptr = data.release()
. See cppreference.
With C++14, the better way to write the allocation is:
auto data = std::make_unique<uint32_t[]>(StagingTextureDesc.Width * StagingTextureDesc.Height);
. This assumes you are fine with a C++ exception being thrown for out-of-memory.
If you want to return an error code for out-of-memory instead of a C++ exception, use:
auto data = std::unique_ptr<uint32_t[]>(new (std::nothrow) uint32_t[StagingTextureDesc.Width * StagingTextureDesc.Height]); if (!data) // return error
将 10:10:10:2 内容转换为 8:8:8:8 内容可以在 CPU 上通过位移有效地完成。
棘手的一点是处理将 2 位 alpha 放大到 8 位。例如,您希望 11
的 Alpha 映射到 255,而不是 192。
这是上面循环的替换
// Assumes our staging texture is DXGI_FORMAT_R10G10B10A2_UNORM
for(UINT y = 0; y < StagingTextureDesc.Height; ++y)
{
auto sptr = reinterpret_cast<uint32_t*>(src);
for(UINT x = 0; x < StagingTextureDesc.Width; ++x)
{
uint32_t t = *(sptr++);
uint32_t r = (t & 0x000003ff) >> 2;
uint32_t g = (t & 0x000ffc00) >> 12;
uint32_t b = (t & 0x3ff00000) >> 22;
// Upscale alpha
// 11xxxxxx -> 11111111 (255)
// 10xxxxxx -> 10101010 (170)
// 01xxxxxx -> 01010101 (85)
// 00xxxxxx -> 00000000 (0)
t &= 0xc0000000;
uint32_t a = (t >> 24) | (t >> 26) | (t >> 28) | (t >> 30);
// Convert to DXGI_FORMAT_R8G8B8A8_UNORM
*(dest++) = r | (g << 8) | (b << 16) | (a << 24);
}
src += mapInfo.RowPitch;
}
当然我们可以组合移位操作,因为我们将它们向下移动然后在上一个循环中返回。我们确实需要更新掩码以删除通常被全班移走的位。这将替换上面循环的内部主体:
// Convert from 10:10:10:2 to 8:8:8:8
uint32_t t = *(sptr++);
uint32_t r = (t & 0x000003fc) >> 2;
uint32_t g = (t & 0x000ff000) >> 4;
uint32_t b = (t & 0x3fc00000) >> 6;
t &= 0xc0000000;
uint32_t a = t | (t >> 2) | (t >> 4) | (t >> 6);
*(dest++) = r | g | b | a;
任何时候减少位深度都会引入错误。 ordered dithering and error-diffusion dithering 等技术通常用于这种性质的像素转换。这些给图像引入了一点噪声,以减少丢失的低位的视觉影响。
For examples of conversions for all
DXGI_FORMAT
types, see DirectXTex which makes use of DirectXMath for all the various packed vector types. DirectXTex also implements both 4x4 ordered dithering and Floyd-Steinberg error-diffusion dithering when reducing bit-depth.