Basic Tutorial 5

From Axiom

Jump to: navigation, search

Basic Tutorial 5: The Ogre startup sequence

  • Original version by Clay Culver
  • Port to Axiom Crickhollow by patientfox

Any problems you encounter while working with this tutorial should be posted to the Crickhollow Forum.

Contents

Prerequisites

This tutorial assumes you have knowledge of C# programming and are able to setup and compile an Mogre application (if you have trouble setting up your application, see Basic Tutorial 1 for a detailed setup walkthrough).

Introduction

In this tutorial I will be walking you through how to setup ogre in your own environment. Ogre has a very specific order in which you need to understand before you can write your own Ogre application from scratch. At the end of this tutorial you know how to set up Ogre in a way which no longer requires the ExampleApplication class to get started.

As you go through the tutorial you should be slowly adding code to your own project and watching the results as we build it.

Getting Started

As with the last tutorial, we will be using a pre-constructed code base as our starting point. Create a new Console Application project and replace the code in Program.cs with the following code:

using System;
using System.Collections.Generic;
using System.IO;
using Axiom;
using Axiom.Collections;
using Axiom.Core;
using Axiom.Configuration;
using Axiom.Graphics;
using Axiom.Input;
using Axiom.Math;
using Axiom.Overlays;
using ExampleApplicationCrickhollow;

namespace AxiomTutorial5
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            AxiomStartup axiom = new AxiomStartup();
            axiom.Go();
        }
    }

    class AxiomStartup
    {
        Root _root = null;
        float ticks = 0;
        RenderWindow _renderWindow;
        SceneManager _sceneManager;

        public void Go()
        {
            CreateRoot();
            DefineResources();
            SetupRenderSystem();
            CreateRenderWindow();
            InitializeResourceGroups();
            CreateScene();
            StartRenderLoop();
        }

        void CreateRoot()
        {
            _root = new Root("AxiomEngine.log");
        }

        void DefineResources()
        {

            string resourceConfigPath = "EngineConfig.xml";

            if (File.Exists(Path.GetFullPath(resourceConfigPath)))
            {
                EngineConfig config = new EngineConfig();

                // load the config file
                // relative from the location of debug and releases executables
                config.ReadXml(resourceConfigPath);

                // interrogate the available resource paths
                foreach (EngineConfig.FilePathRow row in config.FilePath)
                {
                    ResourceGroupManager.Instance.AddResourceLocation(row.src, row.type);
                }
            }

        }

        void SetupRenderSystem()
        {
            if (!ConfigDialog.Configure())
                throw new Exception("The user canceled the configuration dialog.");
        }

        void CreateRenderWindow()
        {
        }

        void InitializeResourceGroups()
        {
        }

        private void CreateScene()
        {
        }

        void StartRenderLoop()
        {
        }
    }
}

NOTE: We are including the ExampleApplicationCrickhollow.cs namespace for the sole purpose of handing graphics configuration. Besides that one detail (which is a hassle to implement from scratch, especially for the purpose of a tutorial), we will be configuration Axiom startup from scratch.

Make sure you can compile and run the application before continuing. If you are having difficulty, refer to the Basic Tutorial 1. Note that this program should do nothing until we add code to it. Additionally, the program won't work until we reach the very end of this tutorial, so unlike previous tutorials you will not be running the program as you go along.

Creating the Root Object

The first thing that any Ogre application needs to do is to create the Root object. Find the CreateRoot function in the OgreStartup class and add the following line of code:

           _root = new Root("AxiomEngine.log");

That's it. Crickhollow Note: Root's constructor takes just one parameter: A path to the log file. Typically this log file is located in the same directory as the executable. In the Hobbiton release of Axiom, Root's ctor takes two params: The location of the EngineConfig.xml file and the target path for the log file.

Defining the Resources

The next thing we need to do is to parse the resource config file and load resources into Axiom. Adding a resource location to ogre is done through a call to a single function:

    ResourceGroupManager.Instance.AddResourceLocation(name, locType, resGroup);

The first parameter of this function is the location of the resource on disk (file/directory name). The second parameter is the type of resource which is generally either "FileSystem" (for a directory) or "Zip" (for a zip file). The last paramter is the resource group to add the resource to. Actually, AddResourceLocation() has several overloads which each take different parameters.

Axiom provides a convenient way of dealing with resources. There is a config file parser class which allows you to place all of your resources into one file. Find the DefineResources function and add this code to it:

           string resourceConfigPath = "EngineConfig.xml";
           if (File.Exists(Path.GetFullPath(resourceConfigPath)))
           {
               EngineConfig config = new EngineConfig();
               // load the config file
               // relative from the location of debug and releases executables
               config.ReadXml(resourceConfigPath);
               // interrogate the available resource paths
               foreach (EngineConfig.FilePathRow row in config.FilePath)
               {
                   ResourceGroupManager.Instance.AddResourceLocation(row.src, row.type);
               }
           }

This is the standard way of parsing the EngineConfig.xml file to populate the ResourceGroupManager. Note that the AddResourceLocation function only tells Axiom where resources are located and what resource groups they are in. This does not parse the scripts and load resources. We will be loading these resources shortly.

Setting up the RenderSystem

Unlike Ogre, Axiom currently does not have a built-in configuration dialog. Fortunately we can use the ConfigDialog.Configure() static method from the ExampleApplicationCrickhollow.cs code to handle engine configuration for us. In lieu of such an automated configuration system, we will briefly cover manually configuring the Axiom engine. In order to use ExampleApplication's config system:

           if (!ConfigDialog.Configure())
               throw new Exception("The user canceled the configuration dialog.");

Alternatively you can manually setup the RenderSystem this way (don't add this code to your project):

           RenderSystem rs = _root.RenderSystems[0];
           rs.SetConfigOption("Full Screen", "No");
           rs.SetConfigOption("Video Mode", "800 x 600 @ 32-bit colour");
           _root.RenderSystem = rs;

Creating the Render Window

Axiom can create the window in which we are rendering for us, or we can do it manually. Find the CreateRenderWindow function and add the following code:

           _renderWindow = _root.Initialize(true, "AxiomDemoWindow");

The Initialize() function is actually a misnomer. This function creates a window to render Axiom in, with the second parameter setting the window title text. If we are trying to embed Axiom into a window which has already been created, we need to use the windows handle which contains it. Assuming the handle was stored in the variable "handle", this would be how you create the RenderWindow (do not add this code to the project):

           NamedParameterList misc = new NamedParameterList();
           misc["externalWindowHandle"] = handle.ToString();
           _renderWindow = _root.CreateRenderWindow("Main RenderWindow", 800, 600, false, misc);

NamedParameterList is a type that comes from the Axiom.Collections namespace.

Initializing Resource Groups

Before we can create the scene and place objects into it, we must first intialize the resources groups which contain our textures, materials, and models. In the LoadResources function, we told Ogre where all of the resources for the program are. Now we need to actually parse those scripts and resources. Failing to call InitializeResourceGroup() or InitializeAllResourceGroups() will result in your program not being able to find the meshes, textures, etc which your program uses.

Before we intialize the resourses we will set the default number of mipmaps which Ogre uses for the textures it loads. Find the InitializeResourceGroups() function and add the following code:

           TextureManager.Instance.DefaultMipmapCount = 5;
           ResourceGroupManager.Instance.InitializeAllResourceGroups();

You can individually initialize each resource group in your application only when it's needed to save startup time and a little memory (use the InitializeResourceGroup function to do this). However, this is not the common approach in most Ogre applications. Note that when you initialize a resource group, it parses and loads scripts, but does not actually load the resources (such as models and textures) into memory until the program actually needs to use it.

You can change this behavior by pre-loading resource groups which you know you will be using. You can do this by calling the LoadResourceGroup(), UnloadResourceGroup() and/or UnloadUnreferencedResourcesInGroup() methods. By carefully selecting which resource groups to place scripts and models in (and manually loading and unloading resources), it's possible to drastically reduce the amount of memory ogre consumes.

For most commons uses of Ogre calling InitializeAllResourceGroups() (and not manually Loading/Unloading resource groups) should be sufficient. If your program is using too much memory or if you are working with a very large scale application/game which uses a very large set of meshes, textures, scripts, and so on, you may want to actually plan out a strategy to deal with your resource allocation.

Creating the Scene

After the RenderWindow is created, we then need to create the SceneManagers, Viewports, and Cameras which the scene uses. We will create a simple scene to view for this tutorial. Add the following code to the CreateScene function:

           _sceneManager = _root.SceneManagers.GetSceneManager(SceneType.Generic);
           Camera cam = _sceneManager.CreateCamera("Camera");
           Viewport vp = _renderWindow.AddViewport(cam);            
           Entity ent = _sceneManager.CreateEntity("ninja", "ninja.mesh");
           _sceneManager.RootSceneNode.CreateChildSceneNode().AttachObject(ent);
           cam.Position = new Vector3(0, 200, -400);
           cam.LookAt(ent.BoundingBox.Center);

This program will render the scene and then terminate itself after 5 seconds has elapsed. I am doing this so that we can avoid trying to handle user input at this time. Unfortunently there is not an easy way to obtain user input in this application without needlessly complicating it, so terminating the program after 5 seconds is the easiest way to solve this problem. In an actual application you should probably use SDL, DirectInput, or some other input system to get user input.

To do this we will need to register a frame listener and keep track of when the application started. Add the following to the end of the CreateScene function:

           _root.FrameStarted += OnFrameEnded;
           ticks = Environment.TickCount;

Finally, we need to add code to the FrameEnded method to terminate the program after the elapsed time (remember that returning false from a frame listener terminates the program):

       void OnFrameEnded(object source, FrameEventArgs e)
       {
           if (Environment.TickCount - ticks > 5000)
               Environment.Exit(0);
       }

The Render Loop

In games (and other applications), it is very common to have Axiom render as fast as the computer can handle (creating the highest FPS possible). Axiom provides a function which will do just that: Root's StartRendering() method. This will keep rendering the scene until the application throws an exception or the Root.Instance.QueueEndRendering() method is called. QueueEndRendering() is a special method used to "short circuit" the rendering process, since (unlike the c++ Ogre engine), FrameListeners in Axiom do not return a bool to indicate whether the engine should continue rendering. To start rendering, find the StartRenderingLoop() method and add the following code:

           _root.StartRendering();

That's it. You may now run the program and see the results.

We can also have Axiom render a single frame by calling the RenderOneFrame() method. This method returns void, so you will have to track on your own whether or not you should continue rendering. A common approach to this would be something like this (do not add this code to your project):

           bool continueRendering = true;
           while (continueRendering)
           {
               _root.RenderOneFrame();
               // doing other things here
               continueRendering = WhetherOrNotToContinueRendering();
           }

This allows you to perform other incremental actions, such as sleeping for a short time to lower the frames per second to a maximum amount (like 60fps).

Conclusion

In this tutorial we have gone over the basics of getting Axiom started as a stand-alone application (with the exception of configuration infrastructure which we cribbed from ExampleApplicationCrickhollow.cs). By this point you should be able to write your own Axiom application which no longer uses the ExampleApplication class.

Source Code

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Axiom;
using Axiom.Core;
using Axiom.Collections;
using Axiom.Configuration;
using Axiom.Graphics;
using Axiom.Input;
using Axiom.Math;
using Axiom.Overlays;
using ExampleApplicationCrickhollow;

namespace AxiomTutorial5
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            AxiomStartup axiom = new AxiomStartup();
            axiom.Go();
        }
    }

    class AxiomStartup
    {
        Root _root = null;
        float ticks = 0;
        RenderWindow _renderWindow;
        SceneManager _sceneManager;

        public void Go()
        {
            CreateRoot();
            DefineResources();
            SetupRenderSystem();
            CreateRenderWindow();
            InitializeResourceGroups();
            CreateScene();
            StartRenderLoop();
        }

        void CreateRoot()
        {
            _root = new Root("AxiomEngine.log");
        }

        void DefineResources()
        {

            string resourceConfigPath = "EngineConfig.xml";

            if (File.Exists(Path.GetFullPath(resourceConfigPath)))
            {
                EngineConfig config = new EngineConfig();

                // load the config file
                // relative from the location of debug and releases executables
                config.ReadXml(resourceConfigPath);

                // interrogate the available resource paths
                foreach (EngineConfig.FilePathRow row in config.FilePath)
                {
                    ResourceGroupManager.Instance.AddResourceLocation(row.src, row.type);
                }
            }
        }

        void SetupRenderSystem()
        {
            if (!ConfigDialog.Configure()) throw new Exception("The user canceled the configuration dialog.");
        }

        void CreateRenderWindow()
        {
            _renderWindow = _root.Initialize(true, "AxiomDemoWindow");
        }

        void InitializeResourceGroups()
        {
            TextureManager.Instance.DefaultMipmapCount = 5;
            ResourceGroupManager.Instance.InitializeAllResourceGroups();
        }

        private void CreateScene()
        {
            _sceneManager = _root.SceneManagers.GetSceneManager(SceneType.Generic);
            Camera cam = _sceneManager.CreateCamera("Camera");
            Viewport vp = _renderWindow.AddViewport(cam);            

            Entity ent = _sceneManager.CreateEntity("ninja", "ninja.mesh");
            _sceneManager.RootSceneNode.CreateChildSceneNode().AttachObject(ent);

            cam.Position = new Vector3(0, 200, -400);
            cam.LookAt(ent.BoundingBox.Center);
        }

        void StartRenderLoop()
        {
            _root.FrameStarted += OnFrameEnded;
            ticks = Environment.TickCount;
            _root.StartRendering();
        }

        void OnFrameEnded(object source, FrameEventArgs e)
        {
            if (Environment.TickCount - ticks > 5000)
            {
                Console.WriteLine(ticks.ToString());
                Root.Instance.QueueEndRendering();
            }
        }

    }
}