RenderTargetBitmap和Viewport3D - 质量问题

16

我想从Viewport3D中导出一个3D场景到位图。

显而易见的方法是使用RenderTargetBitmap -- 但是当我这样做时,导出的位图质量明显低于屏幕上的图像。在网上搜索,似乎RenderTargetBitmap没有利用硬件渲染。这意味着渲染在Tier 0进行。这意味着没有mip-mapping等,因此导出图像的质量降低。

有人知道如何以屏幕质量导出Viewport3D的位图吗?

澄清

虽然下面给出的示例并未显示这一点,我最终需要将Viewport3D的位图导出到文件中。据我所知,唯一的方法是将图像转换为派生自BitmapSource的内容。Cplotts表明,使用RenderTargetBitmap增加导出质量可以改善图像,但由于渲染仍然在软件中完成,因此速度过慢。

有没有一种方法可以使用硬件渲染将渲染的3D场景导出到文件中?这应该是可能的吧?

您可以通过以下xaml看到问题:

<Window x:Class="RenderTargetBitmapProblem.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="400" Width="500">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Viewport3D Name="viewport3D">
            <Viewport3D.Camera>
                <PerspectiveCamera Position="0,0,3"/>
            </Viewport3D.Camera>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <AmbientLight Color="White"/>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <GeometryModel3D>
                        <GeometryModel3D.Geometry>
                            <MeshGeometry3D Positions="-1,-10,0  1,-10,0  -1,20,0  1,20,0"
                                            TextureCoordinates="0,1 0,0 1,1 1,0"
                                            TriangleIndices="0,1,2 1,3,2"/>
                        </GeometryModel3D.Geometry>
                        <GeometryModel3D.Material>
                            <DiffuseMaterial>
                                <DiffuseMaterial.Brush>
                                    <ImageBrush ImageSource="http://www.wyrmcorp.com/galleries/illusions/Hermann%20Grid.png"
                                                TileMode="Tile" Viewport="0,0,0.25,0.25"/>
                                </DiffuseMaterial.Brush>
                            </DiffuseMaterial>
                        </GeometryModel3D.Material>
                    </GeometryModel3D>
                </ModelVisual3D.Content>
                <ModelVisual3D.Transform>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D Axis="1,0,0" Angle="-82"/>
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                </ModelVisual3D.Transform>
            </ModelVisual3D>
        </Viewport3D>
        <Image Name="rtbImage" Visibility="Collapsed"/>
        <Button Grid.Row="1" Click="Button_Click">RenderTargetBitmap!</Button>
    </Grid>
</Window>

而这段代码:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        RenderTargetBitmap bmp = new RenderTargetBitmap((int)viewport3D.ActualWidth, 
            (int)viewport3D.ActualHeight, 96, 96, PixelFormats.Default);
        bmp.Render(viewport3D);
        rtbImage.Source = bmp;
        viewport3D.Visibility = Visibility.Collapsed;
        rtbImage.Visibility = Visibility.Visible;
    }

4
请投票支持RenderTargetBitmap的硬件支持 http://dotnet.uservoice.com/forums/40583-wpf-feature-suggestions/suggestions/1303525-add-optional-hardware-acceleration-to-rendertarget - loraderon
1
我为此投了全部三票。RenderTargets 是 3D 编程和 2D 中最有用的东西之一。 - zezba9000
我从RenderTargetBitmap转移到了BitBlt,并且使用这个函数非常成功。http://stackoverflow.com/a/37386400/690656 - Andreas
6个回答

5

RenderTargetBitmap没有硬件渲染的设置,因此您需要使用Win32或DirectX。我建议使用这篇文章中介绍的DirectX技术。以下是来自该文章的代码示例,展示了如何实现(这是C++代码):

extern IDirect3DDevice9* g_pd3dDevice;
Void CaptureScreen()
{
    IDirect3DSurface9* pSurface;
    g_pd3dDevice->CreateOffscreenPlainSurface(ScreenWidth, ScreenHeight,
        D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &pSurface, NULL);
    g_pd3dDevice->GetFrontBufferData(0, pSurface);
    D3DXSaveSurfaceToFile("Desktop.bmp",D3DXIFF_BMP,pSurface,NULL,NULL);
    pSurface->Release(); 
}

您可以按照以下步骤创建对应于正在呈现WPF内容的Direct3D设备:
  1. 在屏幕图像中的某个点上调用Visual.PointToScreen
  2. 调用User32.dll中的MonitorFromPoint以获得hMonitor
  3. 调用d3d9.dll中的Direct3DCreate9以获取pD3D
  4. 调用pD3D->GetAdapterCount()以计算适配器数目
  5. 从0迭代到count-1,并调用pD3D->GetAdapterMonitor()并将其与先前检索到的hMonitor进行比较以确定适配器索引
  6. 调用pD3D->CreateDevice()来创建设备本身
我可能会在一个单独的C++/CLR库中完成大部分工作,因为这种方法对我来说很熟悉,但您可能会发现使用SlimDX将其转换为纯C#和托管代码更容易。我尚未尝试过这种方法。

4
我不知道mip-mapping是什么(或者软件渲染器是否支持多级抗锯齿),但我记得Charles Petzold之前发了一篇关于打印高分辨率WPF 3D可视化的帖子
我使用你的示例代码进行尝试,看起来效果很好。所以,我认为你只需要稍微放大一下就可以了。
你需要在rtbImage上设置Stretch为None,并修改Click事件处理程序如下:
private void Button_Click(object sender, RoutedEventArgs e)
{
    // Scale dimensions from 96 dpi to 600 dpi.
    double scale = 600 / 96;

    RenderTargetBitmap bmp =
        new RenderTargetBitmap
        (
            (int)(scale * (viewport3D.ActualWidth + 1)),
            (int)(scale * (viewport3D.ActualHeight + 1)),
            scale * 96,
            scale * 96,
            PixelFormats.Default
        );
    bmp.Render(viewport3D);

    rtbImage.Source = bmp;

    viewport3D.Visibility = Visibility.Collapsed;
    rtbImage.Visibility = Visibility.Visible;
}

希望这能解决你的问题!

1
谢谢。这确实解决了我提供的示例问题。然而,渲染仍然由软件进行;我有一个更复杂、更大的场景,在屏幕上渲染非常快。然而,使用RenderTargetBitmap渲染场景需要大约10秒钟,这是不可接受的,并且由于某种原因,只有一半的纹理在300 dpi下被渲染(在600 dpi下会出现内存不足的情况)。肯定有一种方法可以在硬件中渲染到位图上,对吧? - Grokys
1
很高兴上面的例子解决了问题。我没有从你的问题中理解到性能是主要关注点... 我认为这是图像质量。我喜欢LBushkin的建议,我试着玩了一下,现在它可以工作了。我会添加另一个答案。 - cplotts
1
是的,我没有意识到性能是如此重要的问题,直到我发现它需要10秒钟的时间 :) 不过还是谢谢你的回答。 - Grokys

1

我认为您遇到了一些空白屏幕的问题。首先,VisualBrush需要指向可见的视觉元素。其次,也许您忘记了RectangleGeometry需要有尺寸(我刚开始也是这样)。

我确实看到了一些奇怪的东西,我不太理解。也就是说,我不明白为什么我必须在VisualBrush上设置AlignmentY为Bottom。

除此之外,我认为它可以正常工作...我认为您应该很容易地修改代码以适应您的实际情况。

这是按钮单击事件处理程序:

private void Button_Click(object sender, RoutedEventArgs e)
{
    GeometryDrawing geometryDrawing = new GeometryDrawing();
    geometryDrawing.Geometry =
        new RectangleGeometry
        (
            new Rect(0, 0, viewport3D.ActualWidth, viewport3D.ActualHeight)
        );
    geometryDrawing.Brush =
        new VisualBrush(viewport3D)
        {
            Stretch = Stretch.None,
            AlignmentY = AlignmentY.Bottom
        };
    DrawingImage drawingImage = new DrawingImage(geometryDrawing);
    image.Source = drawingImage;
}

这是 Window1.xaml 文件:

<Window
    x:Class="RenderTargetBitmapProblem.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    SizeToContent="WidthAndHeight"
>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="400"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="400"/>
        </Grid.ColumnDefinitions>

        <Viewport3D
            x:Name="viewport3D"
            Grid.Row="0"
            Grid.Column="0"
        >
            <Viewport3D.Camera>
                <PerspectiveCamera Position="0,0,3"/>
            </Viewport3D.Camera>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <AmbientLight Color="White"/>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <GeometryModel3D>
                        <GeometryModel3D.Geometry>
                            <MeshGeometry3D
                                Positions="-1,-10,0  1,-10,0  -1,20,0  1,20,0"
                                TextureCoordinates="0,1 0,0 1,1 1,0"
                                TriangleIndices="0,1,2 1,3,2"
                            />
                        </GeometryModel3D.Geometry>
                        <GeometryModel3D.Material>
                            <DiffuseMaterial>
                                <DiffuseMaterial.Brush>
                                    <ImageBrush
                                        ImageSource="http://www.wyrmcorp.com/galleries/illusions/Hermann%20Grid.png"
                                        TileMode="Tile"
                                        Viewport="0,0,0.25,0.25"
                                    />
                                </DiffuseMaterial.Brush>
                            </DiffuseMaterial>
                        </GeometryModel3D.Material>
                    </GeometryModel3D>
                </ModelVisual3D.Content>
                <ModelVisual3D.Transform>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D Axis="1,0,0" Angle="-82"/>
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                </ModelVisual3D.Transform>
            </ModelVisual3D>
        </Viewport3D>

        <Image
            x:Name="image"
            Grid.Row="0"
            Grid.Column="0"
        />

        <Button Grid.Row="1" Click="Button_Click">Render!</Button>
    </Grid>
</Window>

再次感谢 - 这让我得到了一个DrawingImage,但是DrawingImage不是BitmapSource。我需要BitmapSource的原因是我需要将场景写入文件。很抱歉我在问题中没有指定这一点 - 我认为通过简化问题,我会使它更容易回答...有什么办法可以在不使用RenderTargetBitmap的情况下将DrawingImage写入文件吗? - Grokys
嗯,这个我不清楚……你尝试过使用RenderTargetBitmap将DrawingImage转换为BitmapSource吗?它仍然速度非常慢吗? - cplotts
我问了另一个有经验的人,他也想不出方法...你可能会陷入困境。如果你解决了这个问题,请在这里留言。 - cplotts
好的,谢谢你的帮助。看起来我可能需要尝试使用XNA或DirectX来完成这个。 - Grokys

0

我认为这里的问题确实是WPF的软件渲染器没有执行mip-mapping和多级反锯齿。你可以尝试创建一个DrawingImage,其ImageSource是你想要渲染的3D场景,而不是使用RanderTargetBitmap。理论上,硬件渲染应该会产生场景图像,然后您可以从DrawingImage中编程提取它。


我无法让它工作:我尝试使用VisualBrush将Viewport3D绘制到DrawingVisual中。然后,我将生成的图形放入DrawingImage中,并将其分配给rtbImage.Source,但图像为空白。这是您想要的吗?还有其他想法吗? - Grokys
@Groky:我会尝试制作一个简单的示例,以便您可以根据自己的需求进行调整。 - LBushkin

0
使用SlimDX,尝试访问ViewPort3D渲染到的DirectX表面,
然后执行读取像素以将缓冲区从图形卡的像素缓冲区读入常规内存。
一旦您拥有(非托管的)缓冲区,请使用不安全代码或马歇尔将其复制到现有的可写位图中。

0

我在Windows Presentation Foundation论坛http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/50989d46-7240-4af5-a8b5-d034cb2a498b/上也得到了一些有用的答案。

特别是,我将尝试这两个答案,都来自Marco Zhou:

或者您可以尝试将Viewport3D渲染到离屏HwndSource中,然后获取其HWND,并将其输入到Bitblt函数中。Bitblt将会将硬件光栅化器已经渲染的内容复制回您自己的内存缓冲区。我自己没有尝试过这种方法,但它值得一试,理论上应该可以工作。

我认为一种不需要使用WIN32 API的简单方法是将Viewport3D放置在ElementHost中,并调用其ElementHost.DrawToBitmap()方法。这里有一个注意点,就是你需要在Viewport3D“完全渲染”后正确地调用DrawToBitmap()方法。你可以通过调用System.Windows.Forms.Application.DoEvents()手动处理消息,并连接CompositionTarget.Rendering事件以从合成线程获得回调(由于我不确定Rendering事件在这种典型情况下是否可靠,所以这可能有效)。顺便说一句,上述方法基于这样一个假设,即你不需要在屏幕上显示ElementHost。如果ElementHost将在屏幕上显示,你可以直接调用DrawToBitmap()方法。

这两种解决方案都不起作用。第一种只会在 HwndSource 不在屏幕上时产生黑色窗口。第二种使用软件渲染。 - Grokys

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接