八叶一刀·无仞剑

万物流转,无中生有,有归于无

导航

DirectX读取纹理数据到CPU

Posted on 2020-08-26 21:54  闪之剑圣  阅读(1361)  评论(0编辑  收藏  举报

最近自己在研究一个问题: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, &copy_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);