views:

1144

answers:

6

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?

Here's an example... on-screen quality:

On screen quality

RenderTargetBitmapQuality:

alt text

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

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.

LBushkin
I couldn't get this to work: I tried using a VisualBrush to paint the Viewport3D into a DrawingVisual. I then put the generated drawing into a DrawingImage and assigned this to rtbImage.Source, but the image comes out blank. Is this what you intended? Any other ideas?
Groky
@Groky: I'll see if I can't whip up a simple example that works, which you could adapt to your needs.
LBushkin
@LBushkin: Did you get chance to look into this? I must admit I'm totally stumped...
Groky
+2  A: 

Hi there ... 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!

cplotts
Thanks. That does solve the problem on the example I gave. However, rendering is still being carried out by software; I have a far more complex, bigger scene, which renders very quickly on-screen. However, using RenderTargetBitmap it takes approximately 10 seconds to render the scene, which is unacceptable, and for some reason only half of the texture gets rendered at 300 dpi (at 600 dpi it fails with an Out of Memory). There must be a way to render to a bitmap in hardware, surely?
Groky
Glad it solved the problem in the example above. I didn't understand (from your question) that performance was the main concern ... I thought it was image quality. I like LBushkin's suggestion and I played with it and got it working. I'll add another answer.
cplotts
Yes, I didn't realise that performance was such a concern until I found it was taking 10 seconds :) Thanks for your answer though.
Groky
+1  A: 

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>
cplotts
Thank you again - that gets me a DrawingImage, but DrawingImage is not a BitmapSource. The reason I need a BitmapSource is that I need to write the scene to a file. Apologies that I didn't specify this in the question - I thought that by simplifying the problem I would make it easier to answer... Any idea how to write the DrawingImage to a file without using RenderTargetBitmap?
Groky
Hmmm. That I don't know ... have you actually tried RenderTargetBitmap to convert from the DrawingImage to a BitmapSource? Is it still prohibitively slow?
cplotts
I asked another knowledgeable fellow and he couldn't think of a way either ... you might be up a creek here (without a paddle). Well, definitely comment back here if you figure it out.
cplotts
Ok, thanks for your help. Looks like I may have to try doing this using XNA or DirectX.
Groky
+2  A: 

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.

Ray Burns
A: 
Danny Varod
A: 

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.

Groky
And neither of these solutions work either. The first produces only a black window if the HwndSource is off-screen. The second uses software rendering.
Groky