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 位上。

我的问题:

我试过的:

以下所有代码都在注册到 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),我得到了以下不同颜色:

所以我尝试手动转换颜色以检查它是否与我选择的颜色匹配。 我想过位移来简单地比较值

如果这有帮助,这里是应该捕获的编辑器 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.