当前位置: 首页 > news >正文

将 Direct3D11 在 GPU 中的纹理(Texture2D)导出到内存(Map)或导出成图片文件

Direct3D11 的使用通常不是应用程序唯一的部分,于是使用 Direct3D11 的代码如何与其他模块正确地组合在一起就是一个需要解决的问题。

本文介绍将 Direct3D11 在 GPU 中绘制的纹理映射到内存中,这样我们可以直接观察到此纹理是否是正确的,而不用担心是否有其他模块影响了最终的渲染过程。


本文内容

    • SharpDX
    • 来自于 Direct3D11 的渲染纹理
    • 关键代码(SharpDX.DXGI.Surface.Map)
    • 你可能需要拷贝资源
    • 导出成图片文件

SharpDX

本文的代码会使用到 SharpDX 库,因此,你需要在你的项目当中安装这些 NuGet 包:

<!-- 基础,必装 -->
<PackageReference Include="SharpDX" Version="4.2.0" />
<PackageReference Include="SharpDX.D3DCompiler" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />

<!-- 其他,可选 -->
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" />

来自于 Direct3D11 的渲染纹理

本文不会说如何创建或者获取来自 Direct3D11 的渲染纹理,不过如果你希望了解,可以:

  • 自己创建:WPF 使用封装的 SharpDx 控件
  • 或者从其他进程/模块获取:使用 Direct3D11 的 OpenSharedResource 方法渲染来自其他进程/设备的共享资源(SharedHandle)

本文接下来的内容,是在你已经获得了 SharpDX.Direct3D11.Resource 的引用,或者 SharpDX.Direct3D11.Texture2D 的前提之下。当然,如果你获得了其中任何一个实例,可以通过 COM 组件的 QueryInterface 方法获得其他实例。

var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();
var resource = texture.QueryInterface<SharpDX.Direct3D11.Resource>();

关键代码(SharpDX.DXGI.Surface.Map)

要获得 GPU 中渲染的图片,我们必须要将其映射到内存中才行。而映射到内存中的核心代码是 SharpDX.DXGI.Surface 对象的 Map 方法。

using (var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>())
{
    var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream);
    for (var y = 0; y < surface.Description.Height; y++)
    {
        for (var x = 0; x < surface.Description.Width; x++)
        {
            // 在这里使用位图的像素数据,坐标为 (x, y)。
            // 得到此坐标下的像素指针:
            //     var ptr = ((byte*)map.DataPointer) + y * map.Pitch;
            // 得到此像素的颜色值:
            //     var b = *(ptr + 4 * x);
            //     var g = *(ptr + 4 * x + 1);
            //     var r = *(ptr + 4 * x + 2);
            //     var a = *(ptr + 4 * x + 3);
        }
    }
    dataStream.Dispose();
    surface.Unmap();
}

注意以上代码使用了不安全代码(指针),你需要为你的项目开启不安全代码开关,详见:

  • 如何在 .NET 项目中开启不安全代码(以便启用 unsafe fixed 等关键字)

你可能需要拷贝资源

实际上,在使用上面的代码时,你可能会遇到错误,错误出现在 Map 方法的调用上,描述为“参数错误”。实际上真正检查这里的两个参数时并不能发现究竟是哪个参数出了问题。

实际上出问题的参数是 surface 的实例。

一段 GPU 中的纹理要能够被映射到内存,必须要具有 CPU 的访问权。而是否具有 CPU 访问权在创建纹理的时候就已经确定下来了。

如果前面你得到的纹理是自己创建的,那么恭喜你,你只需要改一下创建纹理的参数就好了。给 Texture2DDescriptionCpuAccessFlags 属性加上 CpuAccessFlags.Read 标识。

desc.CpuAccessFlags = CpuAccessFlags.Read;

但是,如果此纹理不是由你自己创建的,那么就需要拷贝一份新的纹理了。当然,拷贝过程发生在 GPU 中,占用的也是 GPU 专用内存(即显存,如果有的话)。

拷贝需要做到两点:

  1. 创建一个新的 Texture2DDescription(一定要是新的实例,你不能影响原来的实例),然后修改其 CPU 访问权限为 Read
  2. 使用 ImmediateContext 实例的 CopyResource 方法来拷贝资源(此实例可以通过 SharpDX.Direct3D11.Device 来找到)。
var originalDesc = originalTexture.Description;
var desc = new Texture2DDescription
{
    CpuAccessFlags = CpuAccessFlags.Read,
    BindFlags = BindFlags.None,
    Usage = ResourceUsage.Staging,
    Width = originalDesc.Width,
    Height = originalDesc.Height,
    Format = originalDesc.Format,
    MipLevels = 1,
    ArraySize = 1,
    SampleDescription =
    {
        Count = 1,
        Quality = 0
    },
};

var texture2D = new Texture2D(device, desc);
device.ImmediateContext.CopyResource(originalTexture, texture2D);

需要注意,拷贝纹理会额外占用显存,一般不建议这么做,除非你真的有需求一定要 CPU 能够访问到这段纹理。

导出成图片文件

实际上,当你组合起来以上以上方法,你应该能够将纹理导出成图片了。

不过,为了理解更方便一些,我还是将导出成图片的全部代码贴出来:

public static unsafe void MapTexture2DToFile(SharpDX.Direct3D11.Texture2D texture, string fileName)
{
    // 获取 Texture2D 的相关实例。
    var device = texture.Device;
    var originDesc = texture.Description;

    // 创建新的 Texture2D 对象。
    var desc = new Texture2DDescription
    {
        CpuAccessFlags = CpuAccessFlags.Read,
        BindFlags = BindFlags.None,
        Usage = ResourceUsage.Staging,
        Width = originDesc.Width,
        Height = originDesc.Height,
        Format = originDesc.Format,
        MipLevels = 1,
        ArraySize = 1,
        SampleDescription =
        {
            Count = 1,
            Quality = 0
        },
        OptionFlags = ResourceOptionFlags.Shared
    };
    var texture2D = new Texture2D(device, desc);

    // 拷贝资源。
    device.ImmediateContext.CopyResource(texture, texture2D);

    var bitmap = new System.Drawing.Bitmap(desc.Width, desc.Height);
    using (var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>())
    {
        var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream);
        var lines = (int)(dataStream.Length / map.Pitch);
        var actualWidth = surface.Description.Width * 4;
        for (var y = 0; y < desc.Height; y++)
        {
            var h = desc.Height - y;
            var ptr = ((byte*)map.DataPointer) + y * map.Pitch;

            for (var x = 0; x < desc.Width; x++)
            {
                var b = *(ptr + 4 * x);
                var g = *(ptr + 4 * x + 1);
                var r = *(ptr + 4 * x + 2);
                var a = *(ptr + 4 * x + 3);
                bitmap.SetPixel(x, y, System.Drawing.Color.FromArgb(a, r, g, b));
            }
        }
        dataStream.Dispose();
        surface.Unmap();
        bitmap.Save(fileName);
    }
}

如果你是希望以纯软件的方式渲染到 WPF 中(WriteableBitmap),可以参考:

  • WPF 高性能位图渲染 WriteableBitmap 及其高性能用法示例

记得打开不安全代码开关哦!详见:

  • 如何在 .NET 项目中开启不安全代码(以便启用 unsafe fixed 等关键字)

参考资料

  • c++ - How to access pixels data from ID3D11Texture2D? - Stack Overflow
  • SharpDX Directx11 How to add normal mapping ? - Graphics and GPU Programming - GameDev.net
  • directx 11 - How to create bitmap from Surface (SharpDX) - Stack Overflow
  • Desktop Duplication API - Windows applications - Microsoft Docs
  • c# - Reading Datastream sharpDX Error all values are 0 - Stack Overflow
  • SharpDX-Samples/Program.cs at master · sharpdx/SharpDX-Samples

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • .NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表
  • C#/.NET 当我们在写事件 += 和 -= 的时候,方法是如何转换成事件处理器的
  • 清理 git 仓库太繁琐?试试 bfg!删除敏感信息删除大文件一句命令搞定(比官方文档还详细的使用说明)
  • 可集成到文件管理器,一句 PowerShell 脚本发布某个版本的所有 NuGet 包
  • Windows 系统的默认字体是什么?应用的默认字体是什么?
  • C# 8.0 的可空引用类型,不止是加个问号哦!你还有很多种不同的可空玩法
  • 一个简单的方法:截取子类名称中不包含基类后缀的部分
  • 使用 MSBuild Target 复制文件的时候如何保持文件夹结构不变
  • 如何在 MSBuild 中正确使用 % 来引用每一个项(Item)中的元数据
  • 如何将一个 .NET 对象序列化为 HTTP GET 的请求字符串
  • 屏幕边缘上有趣的 1 个像素,看不见、摸不到
  • 在 MSBuild 编译过程中操作文件和文件夹(检查存在/创建文件夹/读写文件/移动文件/复制文件/删除文件夹)
  • 在 WPF 程序中应用 Windows 10 真•亚克力效果
  • 推荐 .NET/C# 开发者安装的几款代码分析插件或对应的代码分析 NuGet 包
  • 在 HTML 超链接上添加可交互的 ToolTip
  • 【162天】黑马程序员27天视频学习笔记【Day02-上】
  • 【干货分享】SpringCloud微服务架构分布式组件如何共享session对象
  • angular2 简述
  • C++11: atomic 头文件
  • HTTP中的ETag在移动客户端的应用
  • input的行数自动增减
  • JavaScript实现分页效果
  • Java编程基础24——递归练习
  • LeetCode541. Reverse String II -- 按步长反转字符串
  • node-sass 安装卡在 node scripts/install.js 解决办法
  • Redis字符串类型内部编码剖析
  • use Google search engine
  • 编写高质量JavaScript代码之并发
  • 手写一个CommonJS打包工具(一)
  • 用mpvue开发微信小程序
  • 在weex里面使用chart图表
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • (NSDate) 时间 (time )比较
  • (第27天)Oracle 数据泵转换分区表
  • (分布式缓存)Redis哨兵
  • (附源码)计算机毕业设计ssm电影分享网站
  • (十三)Maven插件解析运行机制
  • (一)基于IDEA的JAVA基础12
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • (转)linux 命令大全
  • (状压dp)uva 10817 Headmaster's Headache
  • ***检测工具之RKHunter AIDE
  • *p++,*(p++),*++p,(*p)++区别?
  • ..thread“main“ com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.3.1
  • .net 怎么循环得到数组里的值_关于js数组
  • .NET 中的轻量级线程安全
  • .NET6实现破解Modbus poll点表配置文件
  • /dev下添加设备节点的方法步骤(通过device_create)
  • @for /l %i in (1,1,10) do md %i 批处理自动建立目录
  • @SentinelResource详解
  • [ 2222 ]http://e.eqxiu.com/s/wJMf15Ku
  • [ Algorithm ] N次方算法 N Square 动态规划解决
  • [2008][note]腔内级联拉曼发射的,二极管泵浦多频调Q laser——
  • [BT]BUUCTF刷题第8天(3.26)