XNA in a Form: how to display a couple of 3D views
(level : medium)


By default, the XNA 1.0 framework allows you to render 2D and 3D graphics in full screen, or in the entire client area of a simple window (on PC). But what about using it to create tools and editors, written with C# and the Windows Forms, and containing more than one 3D view?

In this article, I'm going to explain how to do this, while still begin able to rely on the ContentManager class to load assets. I will also demonstrate how to write a game loop properly, in order to refresh the views on a regular basis when this is needed (for example, to display animations). Finally, I will revisit my 2003 article an introduction to the High Level Shading Language with a simple example: 2-sided shader, to show how to use a .fx file without the Model and ModelMesh classes.


download the source code for this sample (65 Kb) - XNA 1.0 Refresh
updated version for XNA 2.0 (66 Kb)


Setting up the project

Basically, what we want is a Visual Studio project that uses both the .NET Windows Forms and the XNA framework. There are two ways of doing it, but one is much easier than the other:
Here are the steps for the second method:

Adding some views and controls

First, add a user control (Add | User Control...) to the project, name it XnaView, and compile: this should make it available in the Toolbox. Important: another type of control, a label for example, can not be used instead of a user control, or the view will not be redrawn correctly when a window moves on top of it.

The time has come to open the form in design mode, change a few properties, such as its name, icon, size (1000*540 in the sample), and add some controls as well as a couple of XnaView's. In the code sample, the button "Some Dialog" opens a modal dialog box, that I use to verify the 3D views are refreshed correctly when other windows overlap them. In order to see where the XnaView controls are on the form, you can change their BackColor property. After compiling, you should have something similar to the following picture (note: in the sample, the dimensions of the 3D views don't change if the form gets resized, this is left as an exercise for the reader).


Implementing the user control

The code of the XnaView user control is very simple: since it could be used to display basically anything, it doesn't contain any drawing code itself, but calls a delegate each time the view needs to be refreshed. This is accomplished by overriding the OnPaint method inherited from System.Windows.Forms.UserControl. Also, since we know the control will be completely drawn each time, erasing it first with the backColor is unnecessary, and can be prevented by overriding OnPaintBackGround and making it an empty function.
        public delegate void PaintFunction(XnaView ctrl);
        private PaintFunction paintMe;

        protected override void OnPaint(PaintEventArgs e)
        {
            if(paintMe != null)
                paintMe(this);
        }
        
        protected override void OnPaintBackground(PaintEventArgs e)
        {
        }
Now the real problem is there are 2 views in the Form, which is not the case when using XNA's Game class, which only handles one. In my article: rendering to multiple views with DirectX : CreateAdditionalSwapChain, I showed how to use swap chains to deal with this issue, but swap chains don't exist in XNA. Instead, we can associate each view with a render target and a depth buffer, that will be used to do the rendering, and blit the resulting texture in the user control's window.

After each XnaView control is created by the code generated by the Form designer, its Initialize() function is called to pass it the aforementioned delegate and a clear color, as well as the application's graphics device which is needed to instantiate the render target and the depth buffer. In the following code, the Width and Height properties are inherited from the UserControl class:
        public void Initialize(GraphicsDevice gfxDevice, PaintFunction paintFct, Vector4 clearColor)
        {
            this.paintMe = paintFct;
            this.clearColor = clearColor;

            renderTarget = new RenderTarget2D(gfxDevice, Width, Height, 1, SurfaceFormat.Color);
            depthStencil = new DepthStencilBuffer(gfxDevice, Width, Height, DepthFormat.Depth24);
        }
Note: if the control was resizable, the render target and depth buffer would have to be recreated when the width and height change.
Everything should still compile, but the views are not drawn yet; as we just saw, the next thing we need for that is a graphics device.


Creating a graphics device and clearing the views

Since we are not using the Game class, we have to create the graphics device ourselves. This is easy, just look at the code of the CreateDevice() method in XnaForm.cs. Of course, the width and height of the back buffer must be at least as big as the biggest view you are going to use.

Once the device is created, it can be passed to the two XnaView user controls, as mentioned before. In the sample, these controls also both use the same Blit() method to render their content. Since there is a Draw() function in the form's class, why do the controls use the Blit() one? This is because each view being rendered using a render target, and the result being stored in a texture, it is more efficient to simply display this texture again when the control asks to be redrawn (for example, when the test message box is moved in front of it), instead of re-rendering the same frame from scratch.

In the code of Blit(), you can see that if no texture is associated to a given view, that view is simply cleared (in red). Otherwise, a sprite is used to copy the texture into the back buffer. The last line of the function is the one that makes the empty view or the texture show up on the control.

After these changes, the two views are cleared with a red color.


Using the render targets from a game loop

Let's now have a look at the RenderToTexture() function, which takes a view and a function to render it as parameters. Its code is pretty straightforward: it sets up the graphics device to use the render target and depth buffer of the view, clears the buffers, draws the scene, resolves the render target, and gets the result as a texture.

The Draw() function is the one calling RenderToTexture() for each view, and then Blit() to display the results. But who calls Draw()? We have seen that if the control needs to be refreshed, Blit() gets called; but if the user doesn't do anything, and we want an animation to be played, Draw() needs to be called on a more or less regular basis from a "game loop". You usually don't see the game loop, because it is part of the Game class; fortunately, a small amount of code is enough to implement it in the Form (see the end of XnaForm.cs), and attach it to the OnIdle event. Note: this code wouldn't work on x360 - like the rest of the article anyway, since we are talking about Forms.

Another case when Draw() would be called is obviously if the user did something with the user interface (like clicking a button, for example) that triggers a change in the scene (object added or moved, etc), or the way it is displayed. This is not really demonstrated in the sample, since switching to wireframe mode with the W key, for example, is handled in the Update() function - but if there was no game loop, Draw() would have to be called after the wireframe flag is modified.

The two views are now cleared with their respective background colors. With a bit more code in Application_Idle (and the PerformanceCounter class provided in the sample), an Update() function can be called each frame, with a time parameter that can be used to animate the scene.


Drawing a sprite and a mesh: how to use the Content Pipeline

Let's say we first want to draw a sprite in the smallest of the 2 views; the question is: how do we load its texture using XNA's content pipeline? (Note: in a real application, each view should be handled in its own class, but in the sample the smallViewSprite SpriteBatch and the frontTexture and backTexture Texture2D's are members of the main form, for simplicity's sake).

We start by adding the desired texture to the project, and then write some code to create the sprite and load the texture:
            smallViewSprite = new SpriteBatch(gfxDevice);
            frontTexture = contentMgr.Load<Texture2D>("content/front");
But we don't have a content manager yet, and the constructor of this class requires some object implementing the IServiceProvider interface. In the XNA framework, one class doing exactly that is the GameServiceContainer class, which basically stores a collection of services. This means we can create the content manager like this:
            GameServiceContainer services = new GameServiceContainer();
            contentMgr = new ContentManager(services);
The code compiles, and it would be enough for some types of content, but it crashes when loading the texture: in order to instantiate graphics objects, the content manager needs to find a service implementing the IGraphicsDeviceService interface in the container that was passed to its constructor. Since this interface is pretty simple, we can put a GfxService nested class that satisfies it in the main form:
        class GfxService : IGraphicsDeviceService
        {
            GraphicsDevice gfxDevice;

            public GfxService(GraphicsDevice gfxDevice)
            {
                this.gfxDevice = gfxDevice;
                DeviceCreated = new EventHandler(DoNothing);
                DeviceDisposing = new EventHandler(DoNothing);
                DeviceReset = new EventHandler(DoNothing);
                DeviceResetting = new EventHandler(DoNothing);
            }

            public GraphicsDevice GraphicsDevice
            { get { return gfxDevice; } }

            public event EventHandler DeviceCreated;
            public event EventHandler DeviceDisposing;
            public event EventHandler DeviceReset;
            public event EventHandler DeviceResetting;
            
            void DoNothing(object o, EventArgs args)
            {
            }
        }
All it does is return the graphics device we pass it in the constructor. The four events have to be supported even if they do nothing, because they are part of the interface. The code previously used to create the content manager can now be modified to take advantage of the new class:
            // initialize the content manager
            GfxService gfxService = new GfxService(gfxDevice);
            GameServiceContainer services = new GameServiceContainer();
            services.AddService(typeof(IGraphicsDeviceService), gfxService);
            contentMgr = new ContentManager(services);
Et voila! The texture is loaded correctly, and can be applied to our sprite, in the DrawSmallView() function.

There is nothing special about displaying a mesh in the biggest view. Since this is 3D graphics though, some matrices have to be initialized, and can be dynamically changed in the Update() function to make the object rotate. Note: to prevent the mesh from suddenly turning by a huge amount after a pause happens when the test message box is displayed, the "clock" needs to get fixed, as shown in the someDialog_Click() function (comment out the last line if you want to see what the problem is without it).

Warning: the Draw() method of the SpriteBatch class modifies some render states, which therefore need to be reset correctly before drawing the mesh (or you will get very weird results). The code sample takes care of this, and you can also read the following explanation: http://blogs.msdn.com/shawnhar/archive/2006/11/13/spritebatch-and-renderstates.aspx.


Bonus: 2-sided HLSL shader revisited

The goal of this section is to redo the same thing as in my previous article from 2003 but with XNA, using a .fx file. At the same time, this will show how to use an effect without relying on the Model or ModelMesh classes.

First, the sprite in the small view is replaced by a 3D quad, and the two textures it uses (one for each side) are displayed in the bottom corners of the view (see screenshot at the beginning of this article). The code creating the quad and loading the effect is located in the constructor of the form, and in DrawSmallView() the quad is drawn using the DrawUserPrimitives() method of the graphics device. Notice the vertex declaration has to be set up before that, since it doesn't necessarily match the one that was used to draw the previous object. The matrices from the tiger object are reused to rotate the quad.

The effect is originally a copy from the shaders of the initial article, with only a few parameters renamed, and a technique with two passes added to it. I also changed a few types, so that they better match what is strictly needed (float4 was used almost everywhere in the old version). This works as expected, but like in the original C++ article, the C# code has to change a few parameters between the two passes (the cull mode, the multiplying factor for the normals, and the texture), which makes it less reusable. It would be really cool to get rid of this limitation, and as a matter of fact, the C# part of the program shouldn't even know how many passes there are in the effect (so that any other effect can be used instead of the two-sided one, with no modification). Basically, the final code rendering the quad should look like this:
            doubleSidedFx.Begin();
            foreach(EffectPass pass in doubleSidedFx.CurrentTechnique.Passes)
            {
                pass.Begin();
                gfxDevice.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleFan, quad, 0, 2);
                pass.End();
            }
            doubleSidedFx.End();
Changing the cull mode in the effect is trivial: render states can be initialized in each pass, which is a major reason for using effects. The NormalFactor and baseTex shader variables, on the contrary, can't be assigned a value in the same way, and it looks like we are in a dead end. However, since NormalFactor has a fixed value that is known in advance for each pass, one solution is to pass this value to the vertex shader as a uniform parameter (which means the value is the same for every vertex), instead of using a shader constant.

Of course, the effect doesn't know anything about the Texture2D objects representing the "front" and "back" pictures, so, directly changing texture between the two passes is not possible. The trick this time is to use two samplers, corresponding with the two textures, that can be passed to the pixel shader as a parameter. Having an extra sampler would be problematic in complex shaders already using them all, but it is totally fine here (and remember two-sided objects usually display the same texture on both sides - having different ones was just a personal touch).

That's it! All the code specific to displaying a 2-sided object is now part of the effect, the C# program does not do anything special to use this type of rendering rather than another one.



back to top