MultiViewports using Multiple Cameras

Tutorial initially written by DigitalCyborg. Any questions, comments or complaints should be posted in the OgreDotNet Forum: http://www.ogre3d.org/phpBB2addons/viewforum.php?f=1

Introduction

In this tutorial, we'll build our own custom framework which uses 2 cameras and 2 viewports. The main viewport will be the size of the renderWindow and the secondary viewport will be in the bottom left corner of the renderWindow, in front of the main viewport.

The concept is pretty straight forward, just create a second camera and a second viewport. I played around a little with the Eventhandling code though so that the second viewport uses "mouse look" and locomotion is done in the main viewport with the keys, except that when you turn left or right in the main viewport, the secondary camera also turns the same amount.

In general, multiple viewports are useful for "split screen" multiplayer games, but I have a different objective in mind. (which I choose not to share), but I thought that documenting the code might be useful for others. You could also use it for rear view mirrors,etc. One important thing to note that OpenGL and DirectX only support rectangular viewports.. This means that if you want a circular viewport or something like that, you're probably going to want to use the second camera to render to a texture instead and then use that texture however you want.

I assume that you've either already worked through the rest of the tutorials, or that you are familiar enough with OGRE that you don't need the basics explained.

We're going to start with 2 files. The first file is the framework for the application (which is a modified ExampleApplication) and the second file is the Application.

Starting Code

The first file we'll create is the application file. Notice that there is nothing here yet..
The next file should look REALLY familar to anyone who has examined ExampleApplication.cs
I've included everything except the code for the methods that we will be changing in this tutorial

MultiViewportApp.cs

using System;
 using System.Drawing;
 using OgreDotNet;
 
 namespace OgreDotNetTutorial
 {
    class MultipleViewportApp : MultiViewportFramework
    {   
        static void Main(string[] args)
        {
        }
    }
 }

MultiViewportFramework.cs


using System;
 using System.IO;
 using System.Drawing;
 using Math3D;
 using OgreDotNet;
 
 namespace OgreDotNetTutorial
 {
    /// <summary>
    /// My first custom foundation framework
    /// it sets up 2 cameras, 2 viewports
    /// </summary>
    public abstract class MultiViewportFramework : IDisposable
    {
        protected Root mRoot = null;
        protected RenderWindow mRenderWindow = null;
        protected SceneManager mSceneManager = null;
        protected Camera mMainCamera = null;
        protected Camera mSecondCamera = null;
        protected Viewport mMainViewport = null;
        protected Viewport mSecondViewport = null;
        protected OgreDotNet.EventHandler mEventHandler = null;
        
        protected float mDeltaTime = 0.0f;
        protected bool mDone = false;
 
        protected bool mDebugOverlayVisible = true;
        protected bool mWireFrame = false;
        protected TextureFilterOptions mFilter = TextureFilterOptions.TfoBilinear;
        protected UInt32 mAnisotropy = 1;
 
        protected int mMoveVehicle = 0; 
        protected int mTurnVehicle = 0;
        protected float mMoveScale = 150.0f;
        protected float mRotateScale = 1.0f;
        protected int mScreenShotCount = 0;
        
        /// <summary>
        /// Initializes Ogre and starts rendering.
        /// </summary>
        public virtual void Start()
        {
            if (!Setup()) return;
 
            mRoot.StartRendering();
        }
 
        /// <summary>
        /// Initialises Ogre objects and event handling, loads resources, and calls ExampleApplication.CreateScene().
        /// </summary>
        /// <returns>Returns true if successful, false if unsuccessful</returns>
        /// <remarks>This method should only be called by the user if ExampleApplication.Start() is not called.</remarks>
        /// <seealso>ExampleApplication.Start</seealso>
        protected virtual bool Setup()
        {
            mRoot = new Root();
 
            SetupResources("resources.cfg");
 
            if (!mRoot.ShowConfigDialog())
                return false;
 
            mRenderWindow = mRoot.Initialise(true, "joelthemole2000");
 
            CreateSceneManager();
 
            CreateCameras();
 
            CreateViewPorts();
 
            TextureManager.Instance.SetDefaultNumMipmaps(5);
 
            LoadResources();
 
            CreateEventHandler();
    
            MaterialManager.Instance.SetDefaultTextureFiltering(mFilter);
            MaterialManager.Instance.SetDefaultAnisotropy(mAnisotropy);
 
            CreateScene();
            return true;
        }
 
        /// <summary>
        /// Called in Setup when Resources should be added to ResourceGroupManager
        /// Method which will define the source of resources (other than current folder)
        /// </summary>
        protected virtual void SetupResources(string sFileName)
        {
            //Initialiser.SetupResources(sFileName);
            using (StreamReader sr = new StreamReader(sFileName))
            {
                string secName = "", sLocType, sarchName;
                string line;
                while ((line = sr.ReadLine()) != null)
                {
                    int x = line.IndexOf("#");
                    if (x > -1)
                        line = line.Substring(0, x);
                    line = line.Trim();
                    if (line.Length > 0)
                    {
                        if (line[0] == '[')
                        {
                            secName = line.Substring(1, line.Length - 2);
                        }
                        else if (secName.Length > 0)
                        {
                            x = line.IndexOf("=");
                            if (x <= 0)
                                throw new Exception("Invalid line in resource file " + sFileName);
                            sLocType = line.Substring(0, x);
                            sarchName = line.Substring(x + 1);
                            ResourceGroupManager.getSingleton().addResourceLocation(sarchName, sLocType, secName);
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Called when the SceneManager needs to be created.
        /// </summary>
        protected virtual void CreateSceneManager()
        {
            mSceneManager = mRoot.CreateSceneManager((ushort)SceneType.ExteriorClose);
        }
 
        /// <summary>
        /// Called when the default camera needs to be created.
        /// </summary>
        protected virtual void CreateCameras()
        {
        }
 
        /// <summary>
        /// Called when the default ViewPorts are created.
        /// </summary>
        protected virtual void CreateViewPorts()
        {
        }
         
        ///  <summary>
        /// Optional override method where you can perform resource group loading
        /// Must at least do ResourceGroupManager.getSingleton().initialiseAllResourceGroups();
        /// </summary>
        protected virtual void LoadResources()
        {
            ResourceGroupManager.getSingleton().initialiseAllResourceGroups();
        }
 
        ///  <summary>
        ///called by Setup. create EventHandler and set the Delegates
        /// </summary>
        protected virtual void CreateEventHandler()
        {
            mEventHandler = new OgreDotNet.EventHandler(mRoot, mRenderWindow);
            mEventHandler.SubscribeEvents();
            mEventHandler.FrameStarted += new FrameEventDelegate(FrameStarted);
            mEventHandler.FrameEnded += new FrameEventDelegate(FrameEnded);
            mEventHandler.KeyClicked += new KeyEventDelegate(KeyClicked);
            mEventHandler.KeyPressed += new KeyEventDelegate(KeyPressed);
            mEventHandler.KeyReleased += new KeyEventDelegate(KeyReleased);
            mEventHandler.MouseMoved += new MouseMotionEventDelegate(MouseMotion);
            mEventHandler.MouseClicked += new MouseEventDelegate(MouseClick);
            mEventHandler.MousePressed += new MouseEventDelegate(MousePressed);
            mEventHandler.MouseReleased += new MouseEventDelegate(MouseReleased);
            mEventHandler.MouseDragged += new MouseMotionEventDelegate(MouseDragged);
 
        }
 
        /// <summary>
        /// Called when user defined objects need to be instanced.
        /// </summary>
        protected abstract void CreateScene();
 
        /// <summary>
        /// Called at the start of a rendering frame.
        /// </summary>
        /// <param name="timesincelastframe"></param>
        /// <param name="timesincelastevent"></param>
        /// <returns></returns>
        protected virtual bool FrameStarted(FrameEvent e)
        {
            if (mRenderWindow.Closed || mDone) return false;
 
            mDeltaTime = e.TimeSinceLastFrame;
        }
 
        /// <summary>
        /// Called at the end of a rendering frame.
        /// </summary>
        /// <returns></returns>
        protected virtual bool FrameEnded(FrameEvent e)
        {
            return true;
        }
 
        /// <summary>
        /// Called at the end of a rendering frame.
        /// </summary>
        /// <param name="e">MouseMotionEvent </param>
        protected virtual void MouseMotion(MouseMotionEvent e)
        {
        }
 
        /// <summary>
        /// Called when the mouse moves while a mouse button is down.
        /// </summary>
        /// <param name="e">MouseMotionEvent</param>
        protected virtual void MouseDragged(MouseMotionEvent e)
        {
            this.MouseMotion(e);
        }
 
        /// <summary>
        /// Called when a mouse button is clicked.
        /// </summary>
        /// <param name="e">MouseEvent</param>
        protected virtual void MouseClick(MouseEvent e)
        {
        }
 
        /// <summary>
        /// Called when a mouse button is pressed down.
        /// </summary>
        /// <param name="e">MouseEvent</param>
        protected virtual void MousePressed(MouseEvent e)
        {
        }
 
        /// <summary>
        /// Called when a mouse button is released.
        /// </summary>
        /// <param name="e">MouseEvent</param>
        
        protected virtual void MouseReleased(MouseEvent e)
        {
        }
 
        /// <summary>
        /// Called when a key is clicked.
        /// </summary>
        /// <param name="e">KeyEvent</param>
        protected virtual void KeyClicked(KeyEvent e)
        {
            switch (e.KeyCode)
            {
                case KeyCode.Home:
                    mMainCamera.SetOrientation(Quaternion.Identity);
                    mSecondCamera.SetOrientation(Quaternion.Identity);
                    break;
                case KeyCode.Escape:
                    mDone = true;
                    break;
                case KeyCode.R:
                    mWireFrame = !mWireFrame;
                    if (mWireFrame)
                    {
                        mMainCamera.DetailLevel = PolygonMode.PMWireframe;
                        mSecondCamera.DetailLevel = PolygonMode.PMWireframe;
                    }
                    else
                    {
                        mMainCamera.DetailLevel = PolygonMode.PMSolid;
                        mSecondCamera.DetailLevel = PolygonMode.PMSolid;
                    }
                    break;
                case KeyCode.T:
                    switch (mFilter)
                    {
                        case TextureFilterOptions.TfoBilinear:
                            mFilter = TextureFilterOptions.TfoTrilinear;
                            mAnisotropy = 1;
                            break;
                        case TextureFilterOptions.TfoTrilinear:
                            mFilter = TextureFilterOptions.TfoAnisotropic;
                            mAnisotropy = 8;
                            break;
                        case TextureFilterOptions.TfoAnisotropic:
                            mFilter = TextureFilterOptions.TfoBilinear;
                            mAnisotropy = 1;
                            break;
                    }
                    MaterialManager.Instance.SetDefaultTextureFiltering(mFilter);
                    MaterialManager.Instance.DefaultAnisotropy = mAnisotropy;
                    break;
                case KeyCode.SYSRQ:
                    mRenderWindow.WriteContentsToFile(string.Format("ScreenShot{0}.png", mScreenShotCount++));
                    break;
            }
        }
 
        /// <summary>
        /// Called when a key is pressed down.
        /// e / up    mMoveVehicle forward
        /// s / down  mMoveVehicle backward
        /// a / left  mTurnVehicle left
        /// d / right mTurnVehicle right
        /// w / pgup  mMoveVehicle up
        /// q / pgdn  mMoveVehicle down
        /// </summary>
        /// <param name="e">KeyEvent</param>
        protected virtual void KeyPressed(KeyEvent e)
        {
        }
 
        /// <summary>
        /// Called when a key is released.
        /// </summary>
        /// <param name="e">KeyEvent</param>
        protected virtual void KeyReleased(KeyEvent e)
        {
        }
  
        /// <summary>
        /// Disposes of the ExampleApplication instance.
        /// </summary>
        public virtual void Dispose()
        {
            mRoot.Dispose();
        }
    }
 }

Creating the Cameras



In this tutorial were going to use 2 cameras: One for each viewport. There's nothing really fancy about this code. It just creates 2 cameras at the same position, looking at the same point. I don't think there are any suprises in this code, it goes through the same basic process of creating a camera that you should be familiar with... Create the camera from the SceneManager & set its position and orientation.:

protected virtual void CreateCameras()
        {
            mMainCamera = mSceneManager.CreateCamera("MainCamera");
            mSecondCamera = mSceneManager.CreateCamera("SecondCamera");
 
            // Position it at 50,50, 500  
            mMainCamera.SetPosition(new Vector3(50, 50, 500));
            // Look at 0,0,0
            mMainCamera.LookAt = new Vector3(0, 0, 0);
            mMainCamera.SetNearClipDistance(5);
 
            // Position it at 50,50,500
            mSecondCamera.SetPosition(new Vector3(50, 50, 500));
            // Look at 0,0,0 
            mSecondCamera.LookAt = new Vector3(0, 0, 0);
            mSecondCamera.SetNearClipDistance(5);
        }

Creating the Viewports



Ok, now that we have 2 cameras, we can attach them to viewports. It's not too complicated to do this. We basically just add the viewports to the RenderWindow. The important part of this code is the arguments to AddViewport: The first argument is the camera you want to use for that viewport. The second argument tells the renderwindow the Z order of the viewports. If viewportA has a higher Z order than viewportB, viewportA will always be on top. The next four arguments tell the render window where to position the window. The second argument (left) tells the renderwindow where the left edge of the viewport is, relative to the render window. The third argument (top) works similarly. It tells the render window where the top of the viewport is relative to the renderwindow. The fourth & fifth arguments specify the width & height of the viewport respective to renderwindow.

For this tutorial, the main viewport will use the whole renderWindow, and the Second viewport will use the bottom left corner of the renderwindow, starting 2/3 from the top, and having about 1/3 height and width of the renderwindow.

Sounds pretty simple right?.. here's the code:

protected virtual void CreateViewPorts()
        {
            // Create main viewport full width, full height  
            // Camera Name, Z-Order, Left, Top, Width, Height); 
            mMainViewport = mRenderWindow.AddViewport(mMainCamera, 0, 0.0f, 0.0f, 1.0f, 1.0f);
 
            // Set background color 
            mMainViewport.SetBackgroundColour(Color.Black);
 
            // The aspect ratio should be 4/3 
            mMainCamera.SetAspectRatio(4.0f / 3.0f);
            //    (float)mMainViewport.ActualHeight / (float)mMainViewport.ActualWidth);
 
            // Create second viewport, 1/3 size window, bottom, left side 
            // Camera Name, Z-Order, Left, Top, Width, Height,); 
            mSecondViewport = mRenderWindow.AddViewport(mSecondCamera, 1, 0.0f, 0.66f, 0.33f, 0.33f);
 
            // Set background color 
            mSecondViewport.SetBackgroundColour(Color.Black);
 
            // Alter the camera aspect ratio to match the viewport 
            mSecondCamera.SetAspectRatio(
                (float)mSecondViewport.ActualHeight / (float)mSecondViewport.ActualWidth);
        }

After you've added those 2 functions, you should be almost ready to run the code. We just need to make a small update to main.

Updating MultiViewportApp.cs



Ok, now that we've got our cameras and viewports defined, let's update our application to use our new Framework. You should have noticed that I set the SceneManager to Exterior.close (terrain) earlier. If that's not what you want, go back and change it and make your own CreateScene function as you see fit. You should have also noticed that CreateScene was declared to be an abstract function which means we need to implement it in the application sub-class. If you are just following along in the tutorial, then updating the app isn't too hard.. just create a simple terrain and set the ambient light to ~50% so that we can see it.

Some of you prefer the try-catch-finally method, but I'm going to use the "using" shortcut since it makes the code shorter:

class MultipleViewportApp : MultiViewportFramework
    {
        protected override void CreateScene()
        {
            mSceneManager.SetAmbientLight(Color.FromArgb(127, 127, 127));
            mSceneManager.SetWorldGeometry("terrain.cfg");
        }
        static void Main(string[] args)
        {
            using (MultipleViewportApp app = new MultipleViewportApp())
            {
                app.Start();
            }
        }
    }

Ok. At this point you should be able to run this code. when you run it you should see the terrain and 2 viewports but you won't be able to move around yet or pan the second camera. We'll get to that next.

Moving Around

Since this is my tutorial, I can setup things anyway I like. If you don't like my key settings, change them in your own version of the code. The keybindings that I chose to use are e/up arrow - move forward, s/down arrow - move backwards, a/left arrow - turn left, d/right arrow - turn right, w / pgup increase altitude (move up), q/pgdn decrease altitude (move down)..

In order to accomplish this, I chose to use 2 variables as bitvectors, mMoveVehicle indicates the direction to move the vehicle and mTurnVehicle indicates which way to turn the vehicle. Again, key handling code should look familar to you by now. Set the bits in the variables in keypressed and clear them in KeyReleased:

/// <summary>
        /// Called when a key is pressed down.
        /// e / up    mMoveVehicle forward
        /// s / down  mMoveVehicle backward
        /// a / left  mTurnVehicle left
        /// d / right mTurnVehicle right
        /// w / pgup  mMoveVehicle up
        /// q / pgdn  mMoveVehicle down
        /// </summary>
        /// <param name="e">KeyEvent</param>
        protected virtual void KeyPressed(KeyEvent e)
        {
            switch (e.KeyCode)
            {
                case KeyCode.W:
                case KeyCode.Up:
                    mMoveVehicle |= 1;
                    break;
                case KeyCode.S:
                case KeyCode.Down:
                    mMoveVehicle |= 2;
                    break;
                case KeyCode.PageUp:
                case KeyCode.E:
                    mMoveVehicle |= 4;
                    break;
                case KeyCode.PageDown:
                case KeyCode.Q:
                    mMoveVehicle |= 8;
                    break;
                case KeyCode.A:
                case KeyCode.Left:
                    mTurnVehicle |= 1;
                    break;
                case KeyCode.D:
                case KeyCode.Right:
                    mTurnVehicle |= 2;
                    break;
            }
        }
 
        /// <summary>
        /// Called when a key is released.
        /// </summary>
        /// <param name="e">KeyEvent</param>
        protected virtual void KeyReleased(KeyEvent e)
        {
            switch (e.KeyCode)
            {
                case KeyCode.W:
                case KeyCode.Up:
                    mMoveVehicle &= ~1;
                    break;
                case KeyCode.S:
                case KeyCode.Down:
                    mMoveVehicle &= ~2;
                    break;
                case KeyCode.PageUp:
                case KeyCode.E:
                    mMoveVehicle &= ~4;
                    break;
                case KeyCode.PageDown:
                case KeyCode.Q:
                    mMoveVehicle &= ~8;
                    break;
                case KeyCode.A:
                case KeyCode.Left:
                    mTurnVehicle &= ~1;
                    break;
                case KeyCode.D:
                case KeyCode.Right:
                    mTurnVehicle &= ~2;
                    break;
            }
        }

Now that we've set mMoveVehicle and mTurnVehicle, we need to update FrameStarted to use them. the code that we'll add to the bottom of FrameStarted will check if we need to move or turn and move or turn our camera as requested. We've already done the same thing in other tutorials so the usage of mMoveVehicle should look familiar. Since we have 2 cameras which are always in the same postion, the easiest way to do this is move the first camera and set the position of the second camera to be the same.

if (mMoveVehicle > 0)
            {//mMoveCam bits: 1=forward, 2=backward, 4=up, 8=down
                Vector3 vCamMove = Vector3.Zero;
                float mvscale = mMoveScale * e.TimeSinceLastFrame;
 
                if ((mMoveVehicle & 1) > 0)
                    vCamMove += Vector3.NegativeUnitZ;
                if ((mMoveVehicle & 2) > 0)
                    vCamMove += Vector3.UnitZ;
                if ((mMoveVehicle & 4) > 0)
                    vCamMove += Vector3.UnitY;
                if ((mMoveVehicle & 8) > 0)
                    vCamMove += Vector3.NegativeUnitY;
 
                vCamMove *= mvscale;
                mMainCamera.MoveRelative(vCamMove);
                mSecondCamera.SetPosition(mMainCamera.GetPosition());
            }

Using mTurnVehicle isn't really complicated either, we'll just use the yaw function on both cameras:

//mTurnVehicle : 1=left , 2=right
            if ((mTurnVehicle & 1) > 0)
            {
                mMainCamera.Yaw(new Radian(mRotateScale * 1.0f * e.TimeSinceLastFrame));
                mSecondCamera.Yaw(new Radian(mRotateScale * 1.0f * e.TimeSinceLastFrame));
            }
            if ((mTurnVehicle & 2) > 0)
            {
                mMainCamera.Yaw(new Radian(mRotateScale * -1.0f * e.TimeSinceLastFrame));
                mSecondCamera.Yaw(new Radian(mRotateScale * -1.0f * e.TimeSinceLastFrame));
            }

If you compile and run the application at this point you can move around the terrain and both cameras will move with you.

Adding mouse look to the secondary camera

Having a second camera is cool, but its kind of boring if it always looks at the same thing as the main camera. I chose to add "mouse look" to the second camera, since I thought it would be a good way to play with the second camera. All that's necessary to accomplish this is to use the Yaw * Pitch functions on the secondary camera.. the code is similar to the way we've done it in other tutorials:

protected virtual void MouseMotion(MouseMotionEvent e)
        {
            mSecondCamera.Pitch(new Radian(-e.DeltaY * mDeltaTime * 500.0f));
            mSecondCamera.Yaw(new Radian(-e.DeltaX * mDeltaTime * 500.0f));
        }
 
        protected virtual void MouseDragged(MouseMotionEvent e)
        {
            this.MouseMotion(e);
        }

Well, that's all folks. Here's the completed code for those of you who'd rather just copy & paste.

Completed Code


MultiViewportApp.cs

using System;
 using System.Drawing;
 using OgreDotNet;
 
 namespace OgreDotNetTutorial
 {
    class MultipleViewportApp : MultiViewportFramework
    {
        protected override void CreateScene()
        {
            mSceneManager.SetAmbientLight(Color.FromArgb(127, 127, 127));
            mSceneManager.SetWorldGeometry("terrain.cfg");
        }
        static void Main(string[] args)
        {
            using (MultipleViewportApp app = new MultipleViewportApp())
            {
                app.Start();
            }
        }
    }
 }

MutliViewportFramework.cs


using System;
 using System.IO;
 using System.Drawing;
 using Math3D;
 using OgreDotNet;
 
 namespace OgreDotNetTutorial
 {
    /// <summary>
    /// My first custom foundation framework
    /// it sets up 2 cameras, 2 viewports
    /// </summary>
    public abstract class MultiViewportFramework : IDisposable
    {
        protected Root mRoot = null;
        protected RenderWindow mRenderWindow = null;
        protected SceneManager mSceneManager = null;
        protected Camera mMainCamera = null;
        protected Camera mSecondCamera = null;
        protected Viewport mMainViewport = null;
        protected Viewport mSecondViewport = null;
        protected OgreDotNet.EventHandler mEventHandler = null;
        
        protected float mDeltaTime = 0.0f;
        protected bool mDone = false;
 
        protected bool mDebugOverlayVisible = true;
        protected bool mWireFrame = false;
        protected TextureFilterOptions mFilter = TextureFilterOptions.TfoBilinear;
        protected UInt32 mAnisotropy = 1;
 
        protected int mMoveVehicle = 0; 
        protected int mTurnVehicle = 0;
        protected float mMoveScale = 150.0f;
        protected float mRotateScale = 1.0f;
        protected int mScreenShotCount = 0;
        
        /// <summary>
        /// Initializes Ogre and starts rendering.
        /// </summary>
        public virtual void Start()
        {
            if (!Setup()) return;
 
            mRoot.StartRendering();
        }
 
        /// <summary>
        /// Initialises Ogre objects and event handling, loads resources, and calls ExampleApplication.CreateScene().
        /// </summary>
        /// <returns>Returns true if successful, false if unsuccessful</returns>
        /// <remarks>This method should only be called by the user if ExampleApplication.Start() is not called.</remarks>
        /// <seealso>ExampleApplication.Start</seealso>
        protected virtual bool Setup()
        {
            mRoot = new Root();
 
            SetupResources("resources.cfg");
 
            if (!mRoot.ShowConfigDialog())
                return false;
 
            mRenderWindow = mRoot.Initialise(true, "joelthemole2000");
 
            CreateSceneManager();
 
            CreateCameras();
 
            CreateViewPorts();
 
            TextureManager.Instance.SetDefaultNumMipmaps(5);
 
            LoadResources();
 
            CreateEventHandler();
    
            MaterialManager.Instance.SetDefaultTextureFiltering(mFilter);
            MaterialManager.Instance.SetDefaultAnisotropy(mAnisotropy);
 
            CreateScene();
            return true;
        }
 
        /// <summary>
        /// Called in Setup when Resources should be added to ResourceGroupManager
        /// Method which will define the source of resources (other than current folder)
        /// </summary>
        protected virtual void SetupResources(string sFileName)
        {
            //Initialiser.SetupResources(sFileName);
            using (StreamReader sr = new StreamReader(sFileName))
            {
                string secName = "", sLocType, sarchName;
                string line;
                while ((line = sr.ReadLine()) != null)
                {
                    int x = line.IndexOf("#");
                    if (x > -1)
                        line = line.Substring(0, x);
                    line = line.Trim();
                    if (line.Length > 0)
                    {
                        if (line[0] == '[')
                        {
                            secName = line.Substring(1, line.Length - 2);
                        }
                        else if (secName.Length > 0)
                        {
                            x = line.IndexOf("=");
                            if (x <= 0)
                                throw new Exception("Invalid line in resource file " + sFileName);
                            sLocType = line.Substring(0, x);
                            sarchName = line.Substring(x + 1);
                            ResourceGroupManager.getSingleton().addResourceLocation(sarchName, sLocType, secName);
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Called when the SceneManager needs to be created.
        /// </summary>
        protected virtual void CreateSceneManager()
        {
            mSceneManager = mRoot.CreateSceneManager((ushort)SceneType.ExteriorClose);
        }
 
        /// <summary>
        /// Called when the default camera needs to be created.
        /// </summary>
        protected virtual void CreateCameras()
        {
            mMainCamera = mSceneManager.CreateCamera("MainCamera");
            mSecondCamera = mSceneManager.CreateCamera("SecondCamera");
 
            // Position it at 50,50, 500  
            mMainCamera.SetPosition(new Vector3(50, 50, 500));
            // Look back along -Z 
            mMainCamera.LookAt = new Vector3(0, 0, 0);
            mMainCamera.SetNearClipDistance(5);
 
            // Position it at 50,50,500
            mSecondCamera.SetPosition(new Vector3(50, 50, 500));
            // Look back along Z 
            mSecondCamera.LookAt = new Vector3(0, 0, 0);
            mSecondCamera.SetNearClipDistance(5);
        }
 
        /// <summary>
        /// Called when the default ViewPorts are created.
        /// </summary>
        protected virtual void CreateViewPorts()
        {
            // Create main viewport full width, full height  
            // Camera Name, Z-Order, Left, Top, Width, Height); 
            //mRenderWindow.AddViewport(
            mMainViewport = mRenderWindow.AddViewport(mMainCamera, 0, 0.0f, 0.0f, 1.0f, 1.0f);//0.5f);
 
            // Set background color 
            mMainViewport.SetBackgroundColour(Color.Black);
 
            // The aspect ratio should be 4/3 
            mMainCamera.SetAspectRatio(4.0f / 3.0f);
            //    (float)mMainViewport.ActualHeight / (float)mMainViewport.ActualWidth);
 
            // Create second viewport, 1/3 size window, bottom, left side 
            // Camera Name, Z-Order, Left, Top, Width, Height,); 
            mSecondViewport = mRenderWindow.AddViewport(mSecondCamera, 1, 0.0f, 0.66f, 0.33f, 0.33f);
 
            // Set background color 
            mSecondViewport.SetBackgroundColour(Color.Black);
 
            // Alter the camera aspect ratio to match the viewport 
            mSecondCamera.SetAspectRatio(
                (float)mSecondViewport.ActualHeight / (float)mSecondViewport.ActualWidth);
        }
         
        ///  <summary>
        /// Optional override method where you can perform resource group loading
        /// Must at least do ResourceGroupManager.getSingleton().initialiseAllResourceGroups();
        /// </summary>
        protected virtual void LoadResources()
        {
            ResourceGroupManager.getSingleton().initialiseAllResourceGroups();
        }
 
        ///  <summary>
        ///called by Setup. create EventHandler and set the Delegates
        /// </summary>
        protected virtual void CreateEventHandler()
        {
            mEventHandler = new OgreDotNet.EventHandler(mRoot, mRenderWindow);
            mEventHandler.SubscribeEvents();
            mEventHandler.FrameStarted += new FrameEventDelegate(FrameStarted);
            mEventHandler.FrameEnded += new FrameEventDelegate(FrameEnded);
            mEventHandler.KeyClicked += new KeyEventDelegate(KeyClicked);
            mEventHandler.KeyPressed += new KeyEventDelegate(KeyPressed);
            mEventHandler.KeyReleased += new KeyEventDelegate(KeyReleased);
            mEventHandler.MouseMoved += new MouseMotionEventDelegate(MouseMotion);
            mEventHandler.MouseClicked += new MouseEventDelegate(MouseClick);
            mEventHandler.MousePressed += new MouseEventDelegate(MousePressed);
            mEventHandler.MouseReleased += new MouseEventDelegate(MouseReleased);
            mEventHandler.MouseDragged += new MouseMotionEventDelegate(MouseDragged);
 
            //InputReader inputreader = mEventHandler.GetInputReader();
        }
 
        /// <summary>
        /// Called when user defined objects need to be instanced.
        /// </summary>
        protected abstract void CreateScene();
 
        /// <summary>
        /// Called at the start of a rendering frame.
        /// </summary>
        /// <param name="timesincelastframe"></param>
        /// <param name="timesincelastevent"></param>
        /// <returns></returns>
        protected virtual bool FrameStarted(FrameEvent e)
        {
            if (mRenderWindow.Closed || mDone) return false;
 
            mDeltaTime = e.TimeSinceLastFrame;
 
            if (mMoveVehicle > 0)
            {//mMoveCam bits: 1=forward, 2=backward, 4=up, 8=down
                Vector3 vCamMove = Vector3.Zero;
                float mvscale = mMoveScale * e.TimeSinceLastFrame;
 
                if ((mMoveVehicle & 1) > 0)
                    vCamMove += Vector3.NegativeUnitZ;
                if ((mMoveVehicle & 2) > 0)
                    vCamMove += Vector3.UnitZ;
                if ((mMoveVehicle & 4) > 0)
                    vCamMove += Vector3.UnitY;
                if ((mMoveVehicle & 8) > 0)
                    vCamMove += Vector3.NegativeUnitY;
 
                vCamMove *= mvscale;
                mMainCamera.MoveRelative(vCamMove);
                mSecondCamera.SetPosition(mMainCamera.GetPosition());
            }
            
            //mTurnVehicle : 1=left , 2=right
            if ((mTurnVehicle & 1) > 0)
            {
                mMainCamera.Yaw(new Radian(mRotateScale * 1.0f * e.TimeSinceLastFrame));
                mSecondCamera.Yaw(new Radian(mRotateScale * 1.0f * e.TimeSinceLastFrame));
            }
            if ((mTurnVehicle & 2) > 0)
            {
                mMainCamera.Yaw(new Radian(mRotateScale * -1.0f * e.TimeSinceLastFrame));
                mSecondCamera.Yaw(new Radian(mRotateScale * -1.0f * e.TimeSinceLastFrame));
            }
            return true;
        }
 
        /// <summary>
        /// Called at the end of a rendering frame.
        /// </summary>
        /// <returns></returns>
        protected virtual bool FrameEnded(FrameEvent e)
        {
            return true;
        }
 
        /// <summary>
        /// Called at the end of a rendering frame.
        /// </summary>
        /// <param name="e">MouseMotionEvent </param>
        protected virtual void MouseMotion(MouseMotionEvent e)
        {
            mSecondCamera.Pitch(new Radian(-e.DeltaY * mDeltaTime * 500.0f));
            mSecondCamera.Yaw(new Radian(-e.DeltaX * mDeltaTime * 500.0f));
        }
 
        /// <summary>
        /// Called when the mouse moves while a mouse button is down.
        /// </summary>
        /// <param name="e">MouseMotionEvent</param>
        protected virtual void MouseDragged(MouseMotionEvent e)
        {
            this.MouseMotion(e);
        }
 
        /// <summary>
        /// Called when a mouse button is clicked.
        /// </summary>
        /// <param name="e">MouseEvent</param>
        protected virtual void MouseClick(MouseEvent e)
        {
        }
 
        /// <summary>
        /// Called when a mouse button is pressed down.
        /// </summary>
        /// <param name="e">MouseEvent</param>
        protected virtual void MousePressed(MouseEvent e)
        {
        }
 
        /// <summary>
        /// Called when a mouse button is released.
        /// </summary>
        /// <param name="e">MouseEvent</param>
        
        protected virtual void MouseReleased(MouseEvent e)
        {
        }
 
        /// <summary>
        /// Called when a key is clicked.
        /// </summary>
        /// <param name="e">KeyEvent</param>
        protected virtual void KeyClicked(KeyEvent e)
        {
            switch (e.KeyCode)
            {
                case KeyCode.Home:
                    mMainCamera.SetOrientation(Quaternion.Identity);
                    mSecondCamera.SetOrientation(Quaternion.Identity);
                    break;
                case KeyCode.Escape:
                    mDone = true;
                    break;
                case KeyCode.R:
                    mWireFrame = !mWireFrame;
                    if (mWireFrame)
                    {
                        mMainCamera.DetailLevel = PolygonMode.PMWireframe;
                        mSecondCamera.DetailLevel = PolygonMode.PMWireframe;
                    }
                    else
                    {
                        mMainCamera.DetailLevel = PolygonMode.PMSolid;
                        mSecondCamera.DetailLevel = PolygonMode.PMSolid;
                    }
                    break;
                case KeyCode.T:
                    switch (mFilter)
                    {
                        case TextureFilterOptions.TfoBilinear:
                            mFilter = TextureFilterOptions.TfoTrilinear;
                            mAnisotropy = 1;
                            break;
                        case TextureFilterOptions.TfoTrilinear:
                            mFilter = TextureFilterOptions.TfoAnisotropic;
                            mAnisotropy = 8;
                            break;
                        case TextureFilterOptions.TfoAnisotropic:
                            mFilter = TextureFilterOptions.TfoBilinear;
                            mAnisotropy = 1;
                            break;
                    }
                    MaterialManager.Instance.SetDefaultTextureFiltering(mFilter);
                    MaterialManager.Instance.DefaultAnisotropy = mAnisotropy;
                    break;
                case KeyCode.SYSRQ:
                    mRenderWindow.WriteContentsToFile(string.Format("ScreenShot{0}.png", mScreenShotCount++));
                    break;
            }
        }
 
        /// <summary>
        /// Called when a key is pressed down.
        /// e / up    mMoveVehicle forward
        /// s / down  mMoveVehicle backward
        /// a / left  mTurnVehicle left
        /// d / right mTurnVehicle right
        /// w / pgup  mMoveVehicle up
        /// q / pgdn  mMoveVehicle down
        /// </summary>
        /// <param name="e">KeyEvent</param>
        protected virtual void KeyPressed(KeyEvent e)
        {
            switch (e.KeyCode)
            {
                case KeyCode.W:
                case KeyCode.Up:
                    mMoveVehicle |= 1;
                    break;
                case KeyCode.S:
                case KeyCode.Down:
                    mMoveVehicle |= 2;
                    break;
                case KeyCode.PageUp:
                case KeyCode.E:
                    mMoveVehicle |= 4;
                    break;
                case KeyCode.PageDown:
                case KeyCode.Q:
                    mMoveVehicle |= 8;
                    break;
                case KeyCode.A:
                case KeyCode.Left:
                    mTurnVehicle |= 1;
                    break;
                case KeyCode.D:
                case KeyCode.Right:
                    mTurnVehicle |= 2;
                    break;
            }
        }
 
        /// <summary>
        /// Called when a key is released.
        /// </summary>
        /// <param name="e">KeyEvent</param>
        protected virtual void KeyReleased(KeyEvent e)
        {
            switch (e.KeyCode)
            {
                case KeyCode.W:
                case KeyCode.Up:
                    mMoveVehicle &= ~1;
                    break;
                case KeyCode.S:
                case KeyCode.Down:
                    mMoveVehicle &= ~2;
                    break;
                case KeyCode.PageUp:
                case KeyCode.E:
                    mMoveVehicle &= ~4;
                    break;
                case KeyCode.PageDown:
                case KeyCode.Q:
                    mMoveVehicle &= ~8;
                    break;
                case KeyCode.A:
                case KeyCode.Left:
                    mTurnVehicle &= ~1;
                    break;
                case KeyCode.D:
                case KeyCode.Right:
                    mTurnVehicle &= ~2;
                    break;
            }
        }
  
        /// <summary>
        /// Disposes of the ExampleApplication instance.
        /// </summary>
        public virtual void Dispose()
        {
            mRoot.Dispose();
        }
    }
 }

Credits

Using multiple viewports was originally mentioned by Clay in one of the OGRE tutorials, and there are a couple of threads which discuss how you do it in the Main OGRE forum. Thanks guys!!