| Downloads |
Source - latest release
|
Source - weekly snapshot
|
|
| Documentation |
|
Browse
|
|
Download tarball
|
|
|
|
|
|
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 BUILD_STATIC
#ifdef WIN32
#include <neodevd3d9/link.h>
#endif
#include <neodevopengl/link.h>
#endif
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
{
public:
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;
pkVertex->m_kPosition.Set( -5.0f, -5.0f, -20.0f );
pkVertex->m_kColor.Set( 0.0f, 1.0f, 0.0f, 1.0f );
++pkVertex;
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.
g_pkVertexBuffer->Unlock();
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;
}
g_pkPolygonBuffer->Unlock();
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).
Main loop. Process input events and render frames until shutdown event is received and flag is reset
Core::Get()->GetInputManager()->Process();
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.
}
g_pkRenderDevice->End();
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.
g_pkRenderDevice->Flip();
}
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.
SHUTDOWN:
g_pkVertexBuffer = 0;
g_pkPolygonBuffer = 0;
Core::Get()->DeleteRenderDevice( g_pkRenderDevice );
delete g_pkInputListener;
delete g_pkInputGroup;
Core::Get()->Shutdown();
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.
|
|