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;
}
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):You can create the Direct3D device corresponding to the place where the WPF content is being rendered as follows:
Visual.PointToScreen
on a point within your onscreen imageMonitorFromPoint
inUser32.dll
to get the hMonitorDirect3DCreate9
ind3d9.dll
to get a pD3DpD3D->GetAdapterCount()
to count adapterspD3D->GetAdapterMonitor()
and comparing with the previously retrieved hMonitor to determine the adapter indexpD3D->CreateDevice()
to create the device itselfI 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.
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 anDrawingImage
whoseImageSource
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 theDrawingImage
.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:
Hope that solves your problem!
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:
and
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:
Here is Window1.xaml:
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.