[NeoEngine Logo]
Platform independent Open Source 3D game engine
Source - latest release

Source - weekly snapshot

Download tarball




Projects and extensions

Plan files

Useful Links

Mailing Lists

News archive

SourceForge.net Logo
Tutorial 2 - Render Polygon & Basic Input
Last update: 22/Jan/2004

This tutorial covers the basic render interface and input processing for the engine. New code not in the previous tutorial is in bold.

#include <neoengine/core.h>
#include <neoengine/render.h>
#include <neoengine/logstream.h>
#include <neoengine/input.h>
#include <neoengine/renderprimitive.h>
#include <neoengine/vertex.h>
#include <neoengine/polygon.h>

#ifdef WIN32
#include <neodevd3d9/link.h>
#include <neodevopengl/link.h>

using namespace std;
using namespace NeoEngine;
input.h defines the input subsystem. Input events are produced by input devices (the render device is also an input device to get OS messages to the engine) and distributed to input groups and input objects by an input manager.

We now include renderprimitive.h for the RenderPrimitive object used for passing render operations to the render device. All render operations in NeoEngine ultimately use this class for rendering geometry, although if you use highlevel objects like meshes and sprites, you do not have to use this low-level interface since the objects have easy-to-use methods for rendering.

vertex.h defines the vertex buffer class. All vertex data in NeoEngine is managed through vertex buffers. The vertex buffers allow a flexible vertex format, with optional position, normal, color and texture coordinate data (up to 8 texture coordinate layers with 1, 2 or 3 components each). Vertex buffers can be hardware accelerated by being placed in video or agp mapped RAM.

polygon.h defines the polygon buffer. Polygons in NeoEngine are triangles, and are managed through polygon buffers. Polygon buffers can also be stripified for faster rendering.

Vertex/polygon buffers and materials are reference counted objects and accessed through smart pointers. This allows geometry to be shared by multiple entities in the engine, reducing the memory footprint and speeding up rendering by decreasing the amount of data needed to be sent through the bus and graphics pipeline. You should always access reference counted objects through smart pointers! Failing to do so might result in hard to track down bugs and crashes, as the reference counting scheme deallocates objects in the wrong places. For example, do not use VertexBuffer* as a type, but rather the smart pointer type VertexBufferPtr. All smart pointer types are the base type name with Ptr added to the end.

class InputListener : public InputEntity

                     InputListener( InputGroup *pkGroup ) : InputEntity( pkGroup ) {}

    virtual void     Input( const InputEvent *pkEvent );
Here we define the implementation class for an input entity which will receive all input events from our input group. The Input method is implemented at the end of the tutorial.

RenderDevice      *g_pkRenderDevice    = 0;
RenderCaps         g_kRenderCaps;
LogFileSink        g_kLogFile( "neoengine.log" );
InputGroup        *g_pkInputGroup      = 0;
InputListener     *g_pkInputListener   = 0;
bool               g_bRun              = true;
VertexBufferPtr    g_pkVertexBuffer    = 0;
PolygonBufferPtr   g_pkPolygonBuffer   = 0;
Global objects. Added are the input group and listener, vertex buffer and polygon buffer.

The g_pkInputGroup input group is an object grouping together a number of input entities, which are the final receivers of input events. An input group offer the capability of switching on and off input processing for the whole group of entities, and is usually used by inheritance in a manager object of some sort. Since this is a simple tutorial, we just allocate a standard input group object to do the job for us, and more about input entities and our implementation further down (of which our g_pkInputListener is an object).

g_pkVertexBuffer is the buffer object holding the vertex data for our polygon. Remember, the vertex buffer is a reference counted object and must be accessed through the smart pointer interface

g_pkPolygonBuffer is the buffer object holding the triangle data, which is indices into the vertex buffer. This is also a smart pointer, like the vertex buffer.

g_bRun is a flag set to true while the tutorial should run. The input entity will set this flag to false when the tutorial should quit (when the window is closed).

int main( int argc, char **argv )
  neolog.SetLogThreshold( DEBUG );
  neolog.AttachSink( Core::Get()->GetStdoutSink() );
  neolog.AttachSink( &g_kLogFile );

  Core::Get()->Initialize( argc, argv );

  if( !( g_pkRenderDevice = Core::Get()->CreateRenderDevice( "opengl" ) ) )
    goto SHUTDOWN;

  if( !g_pkRenderDevice->Open( RenderWindow( "Render Polygon & Basic Input", g_kRenderCaps, RenderResolution( 640, 480, 16 ) ) ) )
    goto SHUTDOWN;

  g_kRenderCaps     = g_pkRenderDevice->GetCaps();

  g_pkInputGroup    = new InputGroup;
  g_pkInputListener = new InputListener( g_pkInputGroup );
Create the input group and entity (our listener object) to handle input processing from the user and the OS. If no explicit input manager is given to the constructor of the input group, the core input manager in the engine is used (you can access this by a call to Core::Get()->GetInputManager(), as we will do later on in this tutorial).

The input manager collects input events from any input devices attached to it, and distributes them to all input groups and from there to the actual input objects who listen to the events. The only input device used in this tutorial is the render device itself, which collects events from the OS/window manager.

An input group is a collection of input objects. The input manager distributes the incoming events from input devices to all active input groups connected to it. The input groups then distributes the events to the active input object connected to it. This design allows for easy management of groups of objects, as well as find-grained control over each object.

By passing the input group to our listener object constructor it will attach itself to the group to receive events.

  g_pkVertexBuffer = g_pkRenderDevice->CreateVertexBuffer( Buffer::NORMAL, 3, &DiffuseVertex::s_kDecl );
Create the vertex buffer object. All vertex data in the engine are managed through vertex buffers, which allows a custom and flexible format. Vertex buffers are created by the render device to allow for buffers in video/AGP RAM. The call also allocates 3 vertices with the engine-provided type for a vertex with position and diffuse color data. XYZ indicates we want position and diffuse color data. The engine provides a number of standard formats in vertex.h, but you can create your own format using the vertex declaration class in vertexdecl.h.

  g_pkVertexBuffer->Lock( Buffer::WRITE );
When you want to write to a vertex buffer, you must WRITE lock it, and when done writing, unlock it. This allowed for efficient use of video ram for vertex data. If you want to read from a buffer, you do must READ lock it. You must remember to Unlock() the buffer once you are finshed reading/writing.

    DiffuseVertex *pkVertex = (DiffuseVertex*)g_pkVertexBuffer->GetVertex();

    pkVertex->m_kPosition.Set( 0.0f, 5.0f, -20.0f ); 
    pkVertex->m_kColor.Set( 1.0f, 0.0f, 0.0f, 1.0f );
    pkVertex->m_kPosition.Set( -5.0f, -5.0f, -20.0f ); 
    pkVertex->m_kColor.Set( 0.0f, 1.0f, 0.0f, 1.0f );

    pkVertex->m_kPosition.Set( 5.0f, -5.0f, -20.0f ); 
    pkVertex->m_kColor.Set( 0.0f, 0.0f, 1.0f, 1.0f );
Vertex data is accessed through custom classes. If you use a format provided by the engine, you can use the corresponding class in vertex.h, but if you use a custom format setup by your own code you need to create a class matching the data layout for the vertex declaration and use a pointer to that class to access the data.

We are now done writing to the buffer, and must call Unlock(). Any data pointers and iterators to this buffer are now invalid!

  g_pkPolygonBuffer = g_pkRenderDevice->CreatePolygonBuffer( Buffer::NORMAL, 1 );
Create a polygon buffer. Like vertices, polygons are managed through buffers created by the device. Allocate memory for 1 single polygon.

  g_pkPolygonBuffer->Lock( Buffer::WRITE );
    Polygon *pkPolygon = g_pkPolygonBuffer->GetPolygon();
    pkPolygon->v[0] = 0;
    pkPolygon->v[1] = 1;
    pkPolygon->v[2] = 2;
Set the polygon indices. As with vertex buffers, you must lock it for read and write access. We then use the GetPolygon() method to get a pointer to the polygon object at the specified index (default is the first polygon), and the v[] array on the polygon holds the vertex indices. Polygons are counter-clockwise ordered (the engine uses a right-handed coordinate system).

  while( g_bRun )
Main loop. Process input events and render frames until shutdown event is received and flag is reset

Process input. Call this method on the core manager object to make it collect events from the active input devices attached to it (in this tutorial just the render device), and then distribute all the events to the active input groups attached (our g_pkInputGroup).

The input groups will then distribute the events to the active input objects, which includes our InputListener object g_pkInputListener.

    g_pkRenderDevice->Clear( RenderDevice::COLORBUFFER | RenderDevice::ZBUFFER | RenderDevice::STENCILBUFFER, Color::BLACK, 1.0f, 0 );
Clear all buffers in the render target. The arguments passed are a combination of the buffers to clear, the color to clear color buffer with, the z value to clear z buffer with and the value to clear the stencil buffer with. Even if we know we will render the entire color buffer and theoretically don't need to clear it, it is usually faster on most modern hardware to allow it to clear all buffers and not just single out the z buffer.

  g_pkRenderDevice->Begin( Matrix::IDENTITY );
Start a rendering frame. This will tell the device we want to start rendering a frame, and sets up the pipeline to accept rendering calls. No rendering is allowed outside a Begin()/End() block! The argument passed to the method is the current view matrix for the frame.

When we start using cameras, this will be the inverse transformation matrix of the camera. For now we leave it to identity, which is the same as positioning the camera (viewport) at [0,0,0] and looking down negative Z axis. The engine uses a right-hand coordinate system, which means positive Z is pointing "out" of the screen.

      RenderPrimitive kPrim;

      kPrim.m_ePrimitive      = RenderPrimitive::TRIANGLES;
      kPrim.m_pkVertexBuffer  = g_pkVertexBuffer;
      kPrim.m_pkPolygonBuffer = g_pkPolygonBuffer;
      kPrim.m_uiNumPrimitives = 1;

      g_pkRenderDevice->Render( kPrim );
Setup a new rendering operation. All direct rendering commands to the device is made with render primitive objects. We set the primitive type to triangles (RenderPrimitive::TRIANGLES), the vertex buffer pointer (g_pkVertexBuffer), the polygon buffer pointer (g_pkPolygonBuffer) and finally the number of primitives, which is just one single polygon in this example.

Finally we call the render method on the device, and pass along our render primitive object.

We end the rendering of a frame by calling the End() method on the device, which tells the device that we have finished rendering our frame, and it can flush the rendering pipeline.

We flip buffers to show the current rendered frame as front buffer. The main loop is now done, and we start over rendering the next frame.


  g_pkVertexBuffer  = 0;
  g_pkPolygonBuffer = 0;

  Core::Get()->DeleteRenderDevice( g_pkRenderDevice );	
  delete g_pkInputListener;
  delete g_pkInputGroup;


  return 0;
Delete the vertex and polygon buffers. Smart pointers and reference counting scheme will deal with deallocation, just reset pointers. You must never delete a reference counted object directly! The smart pointers will also cleanup when they are destroyed (for example, when they run out of scope), but since we will Shutdown the engine before these variables do run out of scope and cleanup, we must explicitly set them to null here. All resources must be correctly deallocated prior to shutting down the engine!

Also deallocate the input group and listener objects. The input manager will NOT delete attached devices or groups, so we must do this if we explicitly allocate a object, group or device.

void InputListener::Input( const InputEvent *pkEvent )
  if( ( ( pkEvent->m_iType == IE_KEYDOWN ) && ( pkEvent->m_aArgs[0].m_iData == KC_ESCAPE ) ) ||
      ( ( pkEvent->m_iType == IE_SYSEVENT ) && ( pkEvent->m_aArgs[0].m_iData == IE_SYSEVENT_KILL ) ) )
    g_bRun = false;
Implementation of our input entity method processing input events.

The m_iType field of the event holds the type of the event. IE_KEYDOWN are keyboard keydown events and IE_SYSEVENT are events sent from the system, like the window manager and/or OS. Data for the input event are stored in the m_aArgs array, and the datatype depends on the input event type. For IE_KEYDOWN events, the first element is an integer holding the key code. For IE_SYSEVENT events, the first element is an integer holding the message type. A IE_SYSEVENT_KILL message indicates the application should quit.