最近自己在研究一個問題:DX中給定一張Texture,當數據已經存在於GPU端后,應該如何做才能將紋理的數據讀取到CPU中?
要解決這個問題,首先應當知道DirectX中對於一個Texture的描述,這里我們以2DTexture為例,描述它的數據結構如下:
typedef struct D3D11_TEXTURE2D_DESC
{
UINT Width; // 紋理寬度
UINT Height; // 紋理高度
UINT MipLevels; // 允許的Mip等級數
UINT ArraySize; // 可以用於創建紋理數組,這里指定紋理的數目,單個紋理使用1
DXGI_FORMAT Format; // DXGI支持的數據格式,默認DXGI_FORMAT_R8G8B8A8_UNORM
DXGI_SAMPLE_DESC SampleDesc; // MSAA描述
D3D11_USAGE Usage; // 使用D3D11_USAGE枚舉值指定數據的CPU/GPU訪問權限
UINT BindFlags; // 使用D3D11_BIND_FLAG枚舉來決定該數據的使用類型
UINT CPUAccessFlags; // 使用D3D11_CPU_ACCESS_FLAG枚舉來決定CPU訪問權限
UINT MiscFlags; // 使用D3D11_RESOURCE_MISC_FLAG枚舉
} D3D11_TEXTURE2D_DESC;
typedef struct DXGI_SAMPLE_DESC
{
UINT Count; // MSAA采樣數
UINT Quality; // MSAA質量等級
} DXGI_SAMPLE_DESC;
其中Usage有四種類型:D3D11_USAGE_DEFAULT、D3D11_USAGE_IMMUTABLE、D3D11_USAGE_DYNAMIC、D3D11_USAGE_STAGING,它們的關系如下表所示:
D3D11_USAGE | CPU讀 | CPU寫 | GPU讀 | GPU寫 |
---|---|---|---|---|
D3D11_USAGE_DEFAULT | √ | √ | ||
D3D11_USAGE_IMMUTABLE | √ | |||
D3D11_USAGE_DYNAMIC | √ | √ | ||
D3D11_USAGE_STAGING | √ | √ | √ | √ |
DX中能夠支持CPU讀的只有D3D11_USAGE_STAGING,但是它是不能參與到渲染管線中的,因此BindFlags應當設為0。
除此之外,如果我們希望能對Texture進行CPU讀寫操作,那么CPUAccessFlags也要做相應的設置(設置為D3D11_CPU_ACCESS_WRITE或D3D11_CPU_ACCESS_READ)。
另外值得一提的是,這四種類型中性能最高的其實就是D3D11_USAGE_IMMUTABLE,但是這種類型的數據只能在一開始創建的時候設定,之后就不能更改了。
了解了以上內容后我們就可以知道,對於對於不是D3D11_USAGE_STAGING類型的Texture,我們其實是沒有直接的方法可以將它的數據讀取到CPU中的。但是我們可以曲線救國:創建一個D3D11_USAGE_STAGING類型的Texture a,然后將我們要取數據的Texture b復制到這個Texture a中,最后再讓CPU讀取a的數據,就等於變相讀取了b的數據。
具體可以看看代碼,我們首先創建了一個Texture b,把它設為一個Shader Resource和Render Target,並對它進行了一系列渲染為它賦值:
D3D11_TEXTURE2D_DESC texDesc;
texDesc.Width = w;
texDesc.Height = h;
texDesc.MipLevels = 1;
texDesc.ArraySize = 1;
texDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Usage = D3D11_USAGE_DEFAULT;
texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
texDesc.CPUAccessFlags = 0;
texDesc.MiscFlags = 0;
device->CreateTexture2D(&texDesc, 0, &tex_b);
device->CreateShaderResourceView(tex_b, 0, &irradianceSRV);
device->CreateRenderTargetView(tex_b, 0, &irradianceRTV);
接着,我們要想讀取b的數據到CPU,那么需要創建一個D3D11_USAGE_STAGING的Texture a,並且它的寬高、Format等信息需要和a一致:
ID3D11Resource* res = nullptr;
ID3D11Texture2D* tex_b = nullptr;
//這個SRV注意就是tex_b的SRV
irradianceSRV->GetResource(&res);
res->QueryInterface(&tex_b);
D3D11_TEXTURE2D_DESC desc;
tex_b->GetDesc(&desc);
ID3D11Texture2D* copy_tex;
desc.Usage = D3D11_USAGE_STAGING;
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
device->CreateTexture2D(&desc, 0, ©_tex);
然后要做的,就是進行資源復制,並調用context的Map函數:
context->CopyResource(copy_tex, res);
D3D11_MAPPED_SUBRESOURCE mappedResource;
context->Map(copy_tex, 0, D3D11_MAP_READ, 0, &mappedResource);
通過以上代碼,得到的mappedResource就是讀取到CPU的數據。mappedResource.pData就是從數據的起始地址,mappedResource.RowPitch記錄了這些數據中每一行的大小。
需要注意的一點是,RowPitch是經過內存對齊后的大小,一般是要比圖片一行數據的大小更大。
因此,讀取數據可以參考以下代碼:
int index;
float*img_data = new float[desc.Width * desc.Height * 3];
char* begin_data = reinterpret_cast<char*>(mappedResource.pData);
for(int i=0;i< desc.Height;i++)
for (int j = 0; j < desc.Width; j++)
{
int y = desc.Height -1 - i;
index = y * desc.Width + j;
img_data[index * 3] = *reinterpret_cast<float*>(begin_data + i * mappedResource.RowPitch + j * 16);
img_data[index * 3 + 1] = *reinterpret_cast<float*>(begin_data + i * mappedResource.RowPitch + j * 16 + 4);
img_data[index * 3 + 2] = *reinterpret_cast<float*>(begin_data + i * mappedResource.RowPitch + j * 16 + 8);
}
由於Map函數會封鎖GPU對該數據的訪問權限,最后記得要調用Unmap函數解除限制,並清除相應的臨時對象:
context->Unmap(copy_tex, 0);
SAFE_DELETE(img_data);
SAFE_RELEASE(res);
SAFE_RELEASE(tex);
SAFE_RELEASE(copy_tex);