无XAML控件/窗口下,在Direct2D中绘制WinRT Composition Visual并取像素
我正尝试在Direct2D应用中绘制cppwinrt Lottie动画,但不清楚具体实现方法。请问能否不借助XAML控件和窗口,绘制WinRT::Windows::UI::Composition::Visual动画并获取像素数据?
我已成功加载Lottie动画,代码如下:
auto bmv = winrt::make<AnimatedVisuals::LottieLogo1>(); auto avptr = bmv.TryCreateAnimatedVisual(compositor, diags); m_visual = avptr.RootVisual(); auto progressAnimation = compositor.CreateScalarKeyFrameAnimation(); progressAnimation.Duration(std::chrono::seconds(5)); progressAnimation.IterationBehavior(winrt::Windows::UI::Composition::AnimationIterationBehavior::Forever); progressAnimation.Direction(winrt::Windows::UI::Composition::AnimationDirection::Alternate); auto linearEasing = compositor.CreateLinearEasingFunction(); progressAnimation.InsertKeyFrame(0, 0, linearEasing); progressAnimation.InsertKeyFrame(1, 1, linearEasing); m_visual.Properties().StartAnimation(L"Progress", progressAnimation);
现在我需要将该动画绘制到D2D Bitmap上或获取像素信息。我尝试创建帧池,但未收到帧,相关代码如下:
winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice m_device; { auto d3dDevice = CreateD3DDevice(); auto dxgiDevice = d3dDevice.as<IDXGIDevice>(); if (dxgiDevice) { hr = CreateDirect3DDevice(dxgiDevice.get(), &m_device); } }
m_framePool = winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::Create( m_device, winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, 3, winrt::Windows::Graphics::SizeInt32({ 400, 400 }));
m_session = m_framePool.CreateCaptureSession(winrt::Windows::Graphics::Capture::GraphicsCaptureItem::CreateFromVisual(m_visual));
m_frameArrived = m_framePool.FrameArrived(winrt::auto_revoke, { this, &CLottie::OnFrameArrived });
m_session.StartCapture();
再次询问:能否不使用XAML控件和窗口实现该动画的绘制与数据获取?
更新内容
auto controller = CreateDispatcherQueueController(); winrt::Windows::UI::Composition::Compositor compositor{}; CComPtr<ABI::Windows::UI::Composition::ICompositorInterop> compositorInterop; hr = winrt::get_unknown(compositor)->QueryInterface(&compositorInterop); winrt::com_ptr<ABI::Windows::UI::Composition::ICompositionGraphicsDevice> device = nullptr; hr = compositorInterop->CreateGraphicsDevice(direct2dDevice, device.put()); auto gDevice = device.as<winrt::Windows::UI::Composition::CompositionGraphicsDevice>(); winrt::Windows::Foundation::Size surfaceSize{ 400, 400 }; winrt::Windows::UI::Composition::CompositionDrawingSurface surface = gDevice.CreateDrawingSurface(surfaceSize, winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied); auto brush = compositor.CreateSurfaceBrush(surface); float width = 400.0f, height = 400.0f; winrt::Windows::UI::Composition::SpriteVisual visual = compositor.CreateSpriteVisual(); visual.Brush(brush); visual.Size({ width, height }); winrt::Windows::UI::Composition::ShapeVisual shape = compositor.CreateShapeVisual(); shape.Size({ 100.0f,100.0f }); auto circleGeometry = compositor.CreateEllipseGeometry(); circleGeometry.Radius(winrt::Windows::Foundation::Numerics::float2(30, 30)); auto circleShape = compositor.CreateSpriteShape(circleGeometry); circleShape.FillBrush(compositor.CreateColorBrush(winrt::Windows::UI::Colors::Orange())); circleShape.Offset(winrt::Windows::Foundation::Numerics::float2(50, 50)); shape.Shapes().Append(circleShape); visual.Children().InsertAtTop(shape); auto surfaceInterop = surface.as<ABI::Windows::UI::Composition::ICompositionDrawingSurfaceInterop>(); CComPtr<ID2D1DeviceContext> dc; POINT offset = {}; hr = surfaceInterop->BeginDraw(nullptr, __uuidof(dc), reinterpret_cast<void**>(&dc), &offset); CComPtr<ID2D1Bitmap1> bitmap = nullptr; D2D1_BITMAP_PROPERTIES1 prop; prop.pixelFormat.format = DXGI_FORMAT_B8G8R8A8_UNORM; prop.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED; prop.dpiX = 96; prop.dpiY = 96; prop.bitmapOptions = D2D1_BITMAP_OPTIONS_CPU_READ | D2D1_BITMAP_OPTIONS_CANNOT_DRAW; prop.colorContext = NULL; hr = dc->CreateBitmap(D2D1::SizeU(width, height), NULL, 0, prop, &bitmap); hr = bitmap->CopyFromRenderTarget(NULL, dc, NULL); hr = surfaceInterop->EndDraw();
完全可以不依赖XAML控件和窗口实现需求,核心思路是利用Composition离线渲染结合Direct3D/Direct2D互操作,或者修正Graphics Capture的使用问题。以下是两种可行方案:
方案一:修正Graphics Capture获取帧失败问题
你当前用Graphics Capture没收到帧,核心原因是GraphicsCaptureItem::CreateFromVisual要求目标Visual必须属于一个有激活的Composition Host的树(即使是离线Host),且需要确保Compositor和DispatcherQueue正确初始化。
修复步骤:
确保DispatcherQueue生命周期稳定
离线使用Composition必须先创建DispatcherQueue,你更新代码里的CreateDispatcherQueueController()是对的,但要确保它的生命周期和Compositor、Visual树一致,不能提前销毁。创建离线Composition目标并挂载Lottie Visual
必须把Lottie Visual挂载到根ContainerVisual,并关联到离线CompositionTarget:// 初始化Compositor后创建离线目标 auto target = compositor.CreateCompositionTargetForOffscreen(); auto rootVisual = compositor.CreateContainerVisual(); rootVisual.Size({400, 400}); // 将你的Lottie Visual添加到根视觉树 rootVisual.Children().InsertAtTop(m_visual); target.Root(rootVisual);重新创建Capture Session
用CreateFromVisual传入rootVisual来创建CaptureItem,此时帧池就能正常收到渲染后的帧。在FrameArrived中读取像素数据
在回调里通过帧的Direct3DSurface拿到ID3D11Texture2D,映射到CPU读取像素:void CLottie::OnFrameArrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender, winrt::Windows::Foundation::IInspectable const&) { auto frame = sender.TryGetNextFrame(); auto d3dSurface = frame.Direct3DSurface(); winrt::com_ptr<ID3D11Texture2D> texture; winrt::check_hresult(d3dSurface.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>()->GetInterface(__uuidof(texture), texture.put_void())); // 映射纹理到CPU D3D11_MAPPED_SUBRESOURCE mapped{}; winrt::check_hresult(m_d3dDeviceContext->Map(texture.get(), 0, D3D11_MAP_READ, 0, &mapped)); // mapped.pData即为B8G8R8A8_UNORM格式的像素数据 // 处理数据... m_d3dDeviceContext->Unmap(texture.get(), 0); }
方案二:直接将Composition Visual渲染到D2D Bitmap
利用Composition和Direct2D的互操作,直接把Lottie Visual的渲染结果绘制到可读取的D2D Bitmap上:
实现步骤:
关联CompositionGraphicsDevice与Direct2D设备
确保用同一个Direct3D设备创建CompositionGraphicsDevice和Direct2D设备,你更新代码里的这一步是正确的。创建渲染Surface并渲染Lottie Visual
将Lottie Visual添加到SpriteVisual,再通过CompositionRenderer渲染到DrawingSurface:// 创建SpriteVisual并挂载Lottie Visual winrt::Windows::UI::Composition::SpriteVisual renderVisual = compositor.CreateSpriteVisual(); renderVisual.Size({400, 400}); renderVisual.Children().InsertAtTop(m_visual); // 创建用于渲染的DrawingSurface auto renderSurface = gDevice.CreateDrawingSurface( winrt::Windows::Foundation::Size{400, 400}, winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied); // 渲染Visual到Surface auto renderer = compositor.CreateRenderer(); renderer.RenderTarget(renderSurface); renderer.Render(renderVisual);从Surface复制像素到可CPU读取的D2D Bitmap
通过ICompositionDrawingSurfaceInterop获取D2D DeviceContext,将Surface内容复制到指定Bitmap后读取像素:auto surfaceInterop = renderSurface.as<ABI::Windows::UI::Composition::ICompositionDrawingSurfaceInterop>(); CComPtr<ID2D1DeviceContext> dc; POINT offset{}; winrt::check_hresult(surfaceInterop->BeginDraw(nullptr, __uuidof(dc), reinterpret_cast<void**>(&dc), &offset)); // 创建可CPU读取的Bitmap D2D1_BITMAP_PROPERTIES1 prop{}; prop.pixelFormat = D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED); prop.dpiX = 96; prop.dpiY = 96; prop.bitmapOptions = D2D1_BITMAP_OPTIONS_CPU_READ | D2D1_BITMAP_OPTIONS_CANNOT_DRAW; CComPtr<ID2D1Bitmap1> cpuBitmap; winrt::check_hresult(dc->CreateBitmap(D2D1::SizeU(400, 400), nullptr, 0, prop, &cpuBitmap)); // 复制Surface内容到Bitmap winrt::check_hresult(cpuBitmap->CopyFromRenderTarget(nullptr, dc, nullptr)); // 读取像素数据 D2D1_MAPPED_RECT mappedRect{}; winrt::check_hresult(cpuBitmap->Map(D2D1_MAP_OPTIONS_READ, &mappedRect)); // mappedRect.pBits即为像素数据 cpuBitmap->Unmap(); winrt::check_hresult(surfaceInterop->EndDraw());
关键注意事项
- 所有Composition对象(Compositor、Visuals、Target)的生命周期必须和DispatcherQueue绑定,DispatcherQueueController不能提前销毁。
- Direct3D设备需支持至少Feature Level 11.0,且创建时要开启
D3D11_CREATE_DEVICE_BGRA_SUPPORT标志。 - 确保DispatcherQueue的消息循环正常运行,否则Lottie的Progress动画无法更新。
内容的提问来源于stack exchange,提问作者Nikolay

