Ogre
July 8, 2021

Ogre-next [Tutorial]: basic application

Disclaimer

I am a newbie in 3d, graphics, gamedev, etc. This tutorial is my way of learning of these things.

Intro

The Ogre is 3d rendering engine. This article is dedicated to Ogre-next (2.x version) which is more powerful than first version but has less learning resources found online. In fact, there are API Reference and code samples located in source code:

  • ogre-next\Samples\2.0\Tutorials

Even a basic app that shows an empty scene requires relatively complex setup.

Here we will cover how to build a prototype using CMake which could be used as a template for future projects.

The example is for Windows, but it is not so different for Linux. BTW Macoy Madson has similar blog post making it for Linux. He avoids using CMake, but goes far beyond basic set up.

I also assume you have installed CMake and compiler and git

Build or Install ogre-next

You can start from here. Just download and unpack SDK or build it using scripts provided or manually:

git clone --recurse-submodules https://github.com/OGRECave/ogre-next.git

Go to ogre-next-deps and build it

mkdir build
cd build
cmake -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Debug
cmake --build . --target install --config Debug
cmake --build . --config Release
cmake --build . --target install --config Release

Go to ogre-next folder and make link to build\ogredeps

mkdir build
cd build
cmake -D OGRE_USE_BOOST=0 -D OGRE_CONFIG_THREAD_PROVIDER=0 -D OGRE_CONFIG_THREADS=0 -D OGRE_BUILD_COMPONENT_SCENE_FORMAT=1 -D OGRE_BUILD_SAMPLES2=1 -D OGRE_BUILD_TESTS=1 -D OGRE_DEPENDENCIES_DIR=..\..\ogre-next-deps\build\ogredeps -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Debug
cmake --build . --target install --config Debug
cmake --build . --config Release
cmake --build . --target install --config Release

Note: use the latest version of CMake as there is a bug in configuration.

At this point you should have SDK. It is located in ogre-next\build\sdk

CMake

Create CMake project using your IDE or create a folder with CMakeLists.txt in it.

cmake_minimum_required(VERSION 3.8.0)
project(Test VERSION 0.1.0)
add_executable(${PROJECT_NAME} main.cpp)

Using Ogre is not that different from using other libraries in your projects. It has FindOGRE.cmake so we can use find_package to get all includes, links, etc. All we need is to forward SDK path to CMake like this:

-DOGRE_SDK=F:\Libs\Ogre\ogre-next\build\sdk

if(NOT OGRE_SDK)    
  message(FATAL_ERROR "OGRE SDK is not found!")
endif()
list(APPEND CMAKE_MODULE_PATH "${OGRE_SDK}/CMake")
find_package(OGRE)

Manual says we need:

  • Include OgreMain/include
  • Include Components/Hlms/Common/include
  • Include Components/Hlms/Pbs/include
  • Include Components/Hlms/Unlit/include
  • Include build/Release/include (that's where OgreBuildSettings.h is)
  • Add the link path build/Release/lib/
  • Link against OgreHlmsPbs.lib
  • Link against OgreHlmsUnlit.lib
  • Link against OgreMain.lib
  • Bundle the data files in Samples/Media/Hlms/Common with your application
  • Bundle the data files in Samples/Media/Hlms/Pbs with your application
  • Bundle the data files in Samples/Media/Hlms/Unlit with your application

A brief examination of FindOGRE.cmake shows that the OGRE include directories are defined by OGRE_INCLUDE_DIRS and OGRE_${COMPONENT}_INCLUDE_DIRS

set(PROJECT_INCLUDES    
  "${OGRE_INCLUDE_DIRS}"    
  "${OGRE_HlmsPbs_INCLUDE_DIRS}"    
  "${OGRE_HlmsUnlit_INCLUDE_DIRS}"    
  "${OGRE_HlmsUnlit_INCLUDE_DIRS}/../Common" )

Note: for some reason there is no variable for Hlms/common so add it manually.

Same logic for libraries.

set(PROJECT_LIBS     
  "${OGRE_LIBRARIES}"     
  "${OGRE_HlmsPbs_LIBRARIES}"     
  "${OGRE_HlmsUnlit_LIBRARIES}" )

Add it to the target.

add_executable (${PROJECT_NAME} ${SOURCES} ${HEADERS})
target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_INCLUDES})
target_link_libraries(${PROJECT_NAME} ${PROJECT_LIBS})

HLMS

HLMS stands for "High Level Material System", because for the user, the HLMS means just define the material and start looking at it (no need for coding or shader knowledge!).

There are two options:

  • PBS: Stands for Physically Based Shading. The most common for most of your material needs.
  • Unlit: Basic system with no lighting or skeleton skinning. Great for GUI, billboards, particle FXs. Supports multiple texture layering with photoshop-like blending modes.

It seems to make a "basic" demo app we do not need Hlms at all. But to make something more complicated than an empty window we have to initialize it. So, in order Hlms to work it has to load some data files which are conveniently located in sdk\Media\Hlms. All these share common hierarchy that we will define in configuration file. It can be generated directly in CMake

Create Resources.cfg.in

[Essential]
Zip=DebugPack.zip

[Popular]
FileSystem=./
FileSystem=Materials/Common
FileSystem=Materials/Common/GLSL
FileSystem=Materials/Common/HLSL
FileSystem=Materials/Common/Metal

# Do not load this as a resource. It's here merely to tell the code where
# the Hlms templates are located
[Hlms]
DoNotUseAsResource=./

This file will be copied as is. Doing this we leave us a space for future modifications.

set(DATA_DIR "${CMAKE_BINARY_DIR}/data")
set(RESOURCES_CFG "Resources.cfg")

configure_file("${RESOURCES_CFG}.in" "${DATA_DIR}/${RESOURCES_CFG}")

file( COPY "${OGRE_MEDIA_DIR}/Hlms/Common"  DESTINATION "${DATA_DIR}/Hlms" )
file( COPY "${OGRE_MEDIA_DIR}/Hlms/Pbs"     DESTINATION "${DATA_DIR}/Hlms" )
file( COPY "${OGRE_MEDIA_DIR}/Hlms/Unlit"   DESTINATION "${DATA_DIR}/Hlms" )
file( COPY "${OGRE_MEDIA_DIR}/2.0/scripts/materials/Common" 
      DESTINATION "${DATA_DIR}/Materials" )

Note: RESOURCES_CFG can be also used as a parameter for a header file template. This allows you avoid hardcoded literals as I will do later in source code.

Plug-ins and dynamic libraries

Unless Ogre was build with OGRE_STATIC_LIB, you will have a plug-in for every render system. It means rendering backend will be loaded at runtime. Happily for us all plug-ins are registred automaticaly by providing configuration file to Root.

The same *.cfg.in approch is applicable to plug-ins configuration. Keep in mind that we have two sets of binaries: Debug and Release. Let's make configuration file for each.

Plugins.cfg.in

# Defines plugins to load
# Define plugin folder
PluginFolder=@OGRE_PLUGIN_DIR_REL@

# Define plugins
Plugin=RenderSystem_Direct3D11
Plugin=Plugin_ParticleFX

Plugins_d.cfg.in

# Defines plugins to load
# Define plugin folder
PluginFolder=@OGRE_PLUGIN_DIR_DBG@

# Define plugins
Plugin=RenderSystem_Direct3D11_d
Plugin=Plugin_ParticleFX_d

Now we need to copy cfg and plug-ins according to build configuration. Let's go further and define also a list of dynamic libraries to be shipped with executable.

if(CMAKE_BUILD_TYPE STREQUAL  "Debug")    
  set(DEBUG_POSTFIX "_d")    
  set(OGRE_DLLS         
    "${OGRE_BINARY_DBG}"        
    "${OGRE_HlmsPbs_BINARY_DBG}"         
    "${OGRE_HlmsUnlit_BINARY_DBG}"
    "${OGRE_SDK}/bin/Debug/amd_ags_x64.dll")
else()    
  set(OGRE_DLLS         
    "${OGRE_BINARY_REL}"        
    "${OGRE_HlmsPbs_BINARY_REL}"        
    "${OGRE_HlmsUnlit_BINARY_REL}"        
    "${OGRE_SDK}/bin/Release/amd_ags_x64.dll")
endif()

Note: Windows documentation says

If a DLL has dependencies, the system searches for the dependent DLLs as if they were loaded with just their module names. This is true even if the first DLL was loaded by specifying a full path.

Sadly, this is the case for amd_ags_x64.dll as it is a dependency for RenderSystem_Direct3D11.dll. So even having a full path for plug-in itself it could not be loaded because of its dependency located in same directory but unseen for loading function. Moreover the Ogre source code gives as a hint that we can not specify our own search path

 #define DYNLIB_LOAD( a ) LoadLibraryEx( a, NULL, 0) 
// we can not use LOAD_WITH_ALTERED_SEARCH_PATH with relative paths

For now, it is enough to put all of necessary DLLs to executable location. Let's finally generate cfg and copy them.

set(PLUGINS_CFG "Plugins${DEBUG_POSTFIX}.cfg")
configure_file("${PLUGINS_CFG}.in" "${DATA_DIR}/${PLUGINS_CFG}")

foreach(DLL_NAME ${OGRE_DLLS})    
  file( COPY "${DLL_NAME}"    DESTINATION "${CMAKE_BINARY_DIR}" )
endforeach()

That's pretty much it. After CMake is done with your project you will see all necessary DLLs and data folder in building directory.

Main.cpp

For me the starting point was ogre-next\Samples\2.0\Tutorials\Tutorial00_Basic

It is a bit messy but completely suits us as an example. I moved Hlms part away and removed all platform specific defines to focus solely on Windows.

#include "OgreCamera.h"
#include "OgreRoot.h"
#include "OgreWindow.h"
#include "Compositor/OgreCompositorManager2.h"
#include "OgreWindowEventUtilities.h"

class MyWindowEventListener : public Ogre::WindowEventListener
{    
  bool mQuit;
public:    
  MyWindowEventListener() : mQuit(false) {}    
  virtual void windowClosed(Ogre::Window *rw) { mQuit = true; }
  bool getQuit(void) const { return mQuit; }
};

It is just helper class, notifies us when window is closed.

int main(int argc, const char *argv[])
{    
  using namespace Ogre;
  const String pluginsFolder = "./data/";    
  const String writeAccessFolder = "./";
  
#ifndef OGRE_STATIC_LIB
#if OGRE_DEBUG_MODE    
  const char *pluginsFile = "plugins_d.cfg";
#else    
  const char *pluginsFile = "plugins.cfg";
#endif
#endif    
  Root *root = OGRE_NEW Root(pluginsFolder + pluginsFile,
                             writeAccessFolder + "ogre.cfg",  
                             writeAccessFolder + "Ogre.log");

Here we are using plugins.cfg generated by CMake. Note that there is also ogre.cfg, it would not be created unless you call saveConfig. The example code uses dialog window to choose a rendering system. We already know which to choose, let’s hardcode it as well.

Ogre::RenderSystem *renderSystem = 
  root->getRenderSystemByName("Direct3D11 Rendering Subsystem");    
if (!(renderSystem))    
{        
  printf("Render system not found!\n");        
  return -1;    
}
root->setRenderSystem(renderSystem);

You can set up it using ogre.cfg or manually.

root->getRenderSystem()->setConfigOption("sRGB Gamma Conversion", "Yes");
root->getRenderSystem()->setConfigOption("Full Screen", "No");

Create window.

Window *window = root->initialise(true, "Tutorial 00: Basic");
MyWindowEventListener eventListener;    
WindowEventUtilities::addWindowEventListener(window, &eventListener);

Create SceneManager.

const size_t numThreads = 1u;    
SceneManager *sceneManager = 
  root->createSceneManager(ST_GENERIC, numThreads, "ExampleSMInstance");

Create camera and position it at 500 in Z direction looking back along -Z.

Camera *camera = sceneManager->createCamera("Main Camera");
camera->setPosition(Vector3(0, 5, 15)); 
camera->lookAt(Vector3(0, 0, 0));    
camera->setNearClipDistance(0.2f);    
camera->setFarClipDistance(1000.0f);    
camera->setAutoAspectRatio(true);

Setup a basic compositor with a blue clear colour.

CompositorManager2 *compositorManager = root->getCompositorManager2();    
const String workspaceName("Demo Workspace");    
const ColourValue backgroundColour(0.2f, 0.4f, 0.6f);    
compositorManager->createBasicWorkspaceDef(workspaceName, 
                                           backgroundColour, 
                                           IdString());    
compositorManager->addWorkspace(sceneManager, 
                                window->getTexture(), 
                                camera, 
                                workspaceName, 
                                true);

Main loop that just waiting window to be closed.

bool bQuit = false;
while (!bQuit)    
{        
  WindowEventUtilities::messagePump();        
  bQuit = myWindowEventListener.getQuit() || !root->renderOneFrame();    
}

Clean up.

WindowEventUtilities::removeWindowEventListener(window, &eventListener);
OGRE_DELETE root;    
root = 0;

If you build and run, you will see the window filled blue.

HLMS

Though there is nothing we can use material system for, let's initialize it. Add Hlms.h

#include "OgreConfigFile.h"
#include "OgreArchiveManager.h"
#include "OgreHlmsManager.h"
#include "OgreHlmsPbs.h"
#include "OgreHlmsUnlit.h"

void registerHlms( void )
{    
  using namespace Ogre;
  String resourcePath = "data/";
  ConfigFile cf;    
  cf.load( resourcePath + "Resources.cfg" );    
  String rootHlmsFolder = 
    resourcePath + cf.getSetting( "DoNotUseAsResource", "Hlms", "" );
  if( rootHlmsFolder.empty() )        
    rootHlmsFolder = "./";    
  else if( *( rootHlmsFolder.end() - 1 ) != '/' )        
    rootHlmsFolder += "/";

  auto mng = Root::getSingleton().getHlmsManager();
  mng->registerHlms( CreateHlms<HlmsUnlit>(rootHlmsFolder) );    
  mng->registerHlms( CreateHlms<HlmsPbs>(rootHlmsFolder) );
}

Create an instance of Hlms, specifying the location of the shader templates.

template<class T>
T* CreateHlms(const Ogre::String& rootFolder)
{    
  using namespace Ogre;

  String mainFolderPath;    
  StringVector libraryFoldersPaths;    
  StringVector::const_iterator libraryFolderPathIt;    
  StringVector::const_iterator libraryFolderPathEn;
  ArchiveManager &archiveManager = ArchiveManager::getSingleton();
  T::getDefaultPaths( mainFolderPath, libraryFoldersPaths );    
  ArchiveVec archiveLibraryFolders;    
  Archive *archive = archiveManager.load( rootFolder + mainFolderPath, 
                                          "FileSystem", true );    
  libraryFolderPathIt = libraryFoldersPaths.begin();    
  libraryFolderPathEn = libraryFoldersPaths.end();    
  while( libraryFolderPathIt != libraryFolderPathEn )    
  {        
    Archive *archiveLibrary = 
      archiveManager.load( rootFolder + *libraryFolderPathIt, 
                           "FileSystem", true );        
    archiveLibraryFolders.push_back( archiveLibrary );        
    ++libraryFolderPathIt;    
  }
  T* hlms = OGRE_NEW T( archive, &archiveLibraryFolders );
    
  RenderSystem *renderSystem = Root::getSingletonPtr()->getRenderSystem();
  if( renderSystem->getName() == "Direct3D11 Rendering Subsystem" ) 
  {       
    bool supportsNoOverwriteOnTextureBuffers;        
    renderSystem->getCustomAttribute("MapNoOverwriteOnDynamicBufferSRV",
                                     &supportsNoOverwriteOnTextureBuffers);
    if( !supportsNoOverwriteOnTextureBuffers )        
      hlms->setTextureBufferDefaultSize( 512 * 1024 );
   } 
   return hlms;
}

Note: That setTextureBufferDefaultSize is only for Direct3D11 and if MapNoOverwriteOnDynamicBufferSRV is not supported. Comment from samples says:

Set lower limits 512kb instead of the default 4MB per Hlms in D3D 11.0
and below to avoid saturating AMD's discard limit (8MB) or
saturate the PCIE bus in some low end machines.

I did not found any clarification on this matter, but it seems a common practise.

Last words

Afterward it is not so hard. Now I am planning to add some proper rendering loop, play around with some meshes and materials. I will definitely describe my experience in next article.

If you find any misprints, errors or have any thoughts, please let me know.
Thank you.

You can find whole project on GitHub. each tutorial chapter will have a tag.

© 2021 Pavel Busovikov.