RenderTargetBitmap 和 Viewport3D - 质量问题

发布于 2024-08-19 17:42:37 字数 3679 浏览 4 评论 0原文

我想要将 3D 场景从 Viewport3D 导出​​到位图。

执行此操作的明显方法是使用 RenderTargetBitmap - 但是,当我这样做时,导出的位图的质量明显低于屏幕上的图像。在互联网上查找,似乎 RenderTargetBitmap 没有利用硬件渲染。这意味着渲染是在 第 0 层 完成的。这意味着没有 mip 映射等,因此导出图像的质量会降低。

有谁知道如何以屏幕质量导出 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;
    }

I'm wanting to export a 3D scene from a Viewport3D to a bitmap.

The obvious way to do this would be to use RenderTargetBitmap -- however when I this the quality of the exported bitmap is significantly lower than the on-screen image. Looking around on the internet, it seems that RenderTargetBitmap doesn't take advantage of hardware rendering. Which means that the rendering is done at Tier 0. Which means no mip-mapping etc, hence the reduced quality of the exported image.

Does anyone know how to export a bitmap of a Viewport3D at on-screen quality?

Clarification

Though the example given below doesn't show this, I need to eventually export the bitmap of the Viewport3D to a file. As I understand the only way to do this is to get the image into something that derives from BitmapSource. Cplotts below shows that increasing the quality of the export using RenderTargetBitmap improves the image, but as the rendering is still done in software, it is prohibitively slow.

Is there a way to export a rendered 3D scene to a file, using hardware rendering? Surely that should be possible?

You can see the problem with this 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>

And this code:

    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;
    }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

歌枕肩 2024-08-26 17:42:37

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 code> 获取 pD3D
  4. 调用 pD3D->GetAdapterCount() 对适配器进行计数
  5. 从 0 迭代到 count-1 并调用 pD3D->GetAdapterMonitor() 并与之前检索到的 hMonitor 来确定适配器索引
  6. 调用 pD3D->CreateDevice() 来创建设备本身

我可能会在用 C++/CLR 编码的单独库中完成大部分工作,因为这种方法很熟悉对我来说,但您可能会发现使用 SlimDX 将其转换为纯 C# 和托管代码很容易。 我还没有还没试过。

There is no setting on RenderTargetBitmap to tell it to render using hardware, so you will have to fall back to using Win32 or DirectX. I would recommend using the DirectX technique given in this article. The following code from the article and shows how it can be done (this is C++ code):

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(); 
}

You can create the Direct3D device corresponding to the place where the WPF content is being rendered as follows:

  1. Calling Visual.PointToScreen on a point within your onscreen image
  2. Calling MonitorFromPoint in User32.dll to get the hMonitor
  3. Calling Direct3DCreate9 in d3d9.dll to get a pD3D
  4. Calling pD3D->GetAdapterCount() to count adapters
  5. Iterating from 0 to count-1 and calling pD3D->GetAdapterMonitor() and comparing with the previously retrieved hMonitor to determine the adapter index
  6. Calling pD3D->CreateDevice() to create the device itself

I would probably do most of this in a separate library coded in C++/CLR because that approach is familiar to me, but you may find it easy to translate it to pure C# and managed code using using SlimDX. I haven't tried that yet.

↘紸啶 2024-08-26 17:42:37

我不知道 mip 映射是什么(或者软件渲染器是否执行此操作和/或多级抗锯齿),但我确实记得 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;
}

希望能解决您的问题!

I don't know what mip-mapping is (or whether the software renderer does that and/or multi-level anti-aliasing), but I do recall a post by Charles Petzold a while ago that was all about printing hi-res WPF 3D visuals.

I tried it out with your sample code and it appears to work great. So, I assume you just needed to scale things up a bit.

You need to set Stretch to None on the rtbImage and modify the Click event handler as follows:

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;
}

Hope that solves your problem!

任谁 2024-08-26 17:42:37

我认为您出现黑屏的原因有几个。首先,VisualBrush 需要指向可见的 Visual。其次,也许您忘记了 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>

I think you were getting a blank screen for a couple reasons. First, the VisualBrush needed to be pointing to a visible Visual. Second, maybe you forgot that the RectangleGeometry needed to have dimensions (I know I did at first).

I did see some odd things that I don't quite understand. That is, I do not understand why I had to set AlignmentY to Bottom on the VisualBrush.

Other than that, I think it works ... and I think you should easily be able to modify the code for your real situation.

Here is the button click event handler:

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;
}

Here is 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>
桃气十足 2024-08-26 17:42:37

我认为这里的问题确实是 WPF 的软件渲染器不执行 mip 映射和多级抗锯齿。您可以创建一个 DrawingImage,其 ImageSource 是您要渲染的 3D 场景,而不是使用 RanderTargetBitmap。理论上,硬件渲染应该生成场景图像,然后您可以通过编程方式从 DrawingImage 中提取该图像。

I think the issue here indeed is that the software renderer for WPF does not perform mip-mapping and multi-level anti aliasing. Rather than using RanderTargetBitmap, you may be able to create an DrawingImage whose ImageSource is the 3D scene you want to render. In theory, the hardware render should produce the scene image, which you can then programmatically extract from the DrawingImage.

杀手六號 2024-08-26 17:42:37

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

Using SlimDX, try accessing the DirectX surface that the ViewPort3D renders to,

then performing a read-pixel to read the buffer from the graphics card's pixel buffer into regular memory.

Once you have the (unmanaged) buffer, copy it into an existing writable bitmap using unsafe code or marshalling.

热血少△年 2024-08-26 17:42:37

我还在 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 将复制已经存在的内容
由硬件光栅器渲染
返回到您自己的内存缓冲区。我
我自己没有尝试这种方法,但是
理论上值得尝试
说起来,应该可以。

我认为有一种简单的方法,无需使用 pinvoking
进入 WIN32 API 的方法是放置
Viewport3D导入ElementHost,并调用
它的ElementHost.DrawToBitmap(),一
这里需要注意的是你需要打电话
在正确的时间调用 DrawToBitmap()
Viewport3D“完全”之后
渲染”,您可以手动泵送
通过呼叫发送消息
System.Windows.Forms.Application.DoEvents(),
并连接
CompositionTarget.Rendering 事件
从组合中获取回调
线程(这可能有效,因为我不是
确切地确定渲染事件是否是
在这个典型的可靠
环境)。顺便说一句,上述方法
基于您的假设
不需要显示ElementHost
在屏幕上。 ElementHost 将是
显示在屏幕上,您可以
直接调用DrawToBitmap()
方法。

I've also had a few useful answers to this question over at the Windows Presentation Foundation forums at http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/50989d46-7240-4af5-a8b5-d034cb2a498b/.

In particular, I'm going to try these two answers, both from Marco Zhou:

Or you could try rendering the
Viewport3D into a off-screen
HwndSource, and then grab its HWND,
and feed it into Bitblt function . The
Bitblt will copy what's already
rendered by the hardware rasterizer
back to your own in memory buffer. I
am not trying this method myself, but
it's worth trying, and theoretically
speaking, it should work.

and

I think one easy way without pinvoking
into the WIN32 API is to place the
Viewport3D into ElementHost, and call
its ElementHost.DrawToBitmap(), one
caveat here is that you need to call
the DrawToBitmap() at the right time
after the Viewport3D is "fully
rendered", you could manually pump the
messages by calling
System.Windows.Forms.Application.DoEvents(),
and hook up
CompositionTarget.Rendering event to
get callback from the composition
thread (This might work since I am not
exactly sure if the Rendering event is
reliable in this typical
circumstance). BTW, the above method
is based on the assumption that you
don't need to display the ElementHost
on the screen. The ElementHost will be
displayed on the screen, you could
directly call the DrawToBitmap()
method.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文