Note: This article was written in 2009 and some things may have changed.
Introduction
This tutorial will show you how to use the Irrlicht graphics engine to load quake levels into Cinema4D (including the mesh, materials and textures) and then export that back out again to the Irrlicht graphics engine to preview how your game level will look. You will learn about creating polygon objects, materials and textures in Cinema4D and how to access the data again for exporting.
Note: I have just realized that my code has been messed up a little bit by posting to the website. Some of the loops might not look correct due to the removal of less than and greater than brackets. I will fix this up at a later date, for now just refer to the code in the attachment at the end of this article.
Getting Setup
Before we start you will need to setup your development environment. For this tutorial we are using Cinema4D 11.5, Irrlicht 1.6 and Microsoft Visual Studio C++ 2008 Express Edition. And in this tutorial we are running under Vista and Windows 7, but other operating systems should still work. Its best if you follow through the steps in the Getting started with the Cinema4D SDK and Visual Studio C++ 2008 Express Edition tutorial to get yourself setup. The important thing to remember is that you must copy across the DebugWin32.vsprops file in order to compile the plugin, read the tutorial to find out what to do.
Download the attachment at the end of this article and extract it to your Cinema4D 11.5\plugins directory. There should now be a irrlichtviewer folder directly within the plugins directory which contains the projects files. If instead you have a irrlichtviewer folder within a irrlichtviewer folder then you will need to copy the files down one level. This sometimes happens depending on how you do your unzipping.
You will now have to copy the Irrlicht.dll file down a couple of levels next to where Cinema4D is installed. On my machine it is located here… “C:\Program Files\MAXON\CINEMA 4D R11.5″
Note: If you don’t want to compile just yet and only want to run the project then you can just start Cinema4D now skip to the Running the Project section below.
Right click on Visual Studio and choose “Run as Administrator” to run it with the ability to make changes to files within the “Programs Files” folder, which is most likely where your copy of Cinema4D got installed.
When you have installed Irrlicht onto your machine you will need to update the project paths to make sure it can find the right files.
- Right Click on the”irrlichtviewer” project and choose “properties”.
- Open up the “Configuration Properties->C/C++-> General” section.
- At the top you will see the “Additional Include Directories”. It will contain the text “C:\Projects\3rdParty\irrlicht\include”.
- Change this to the include path of your copy of Irrlicht and click Apply.
- Next go to the “Configuration Properties->Linker->General” section.
- Find the text in “Additional Library Directories”. It will currently read “C:\Projects\3rdParty\irrlicht\lib\Win32-visualstudio”.
- Change this to point to the same location of win32-visualstudio as in your lirrlicht install.
- When done press OK on the dialog to save your changes.
Right you should now be all setup. From the Build menu choose “Build Solution”.
If this doesn’t build then you may have missed something in the tutorial on setting up with Visual Studio. Most likely if you are first time user you might not have seen the part on how to fixup the “_api” project. Check that tutorial now and then try building your solution again.
Running the Project
Now that you have the project compiling you can run the project. Select Debug->Start Debugging from Visual Studio. You will now be prompted for an executable file. Browse to the location of your Cinema4D.exe that you installed. Mine is installed here… “C:\Program Files\MAXON\CINEMA 4D R11.5″. Make sure to select “Cinema4d.exe” and NOT the 64bit version.
Cinema4D should now be running. If you got an error about Irrlicht.dll then make sure you have copied it to the same location as your Cinema4D.exe
From the Plugins menu choose “IrrlichtViewer->LoadQuake”. Move the viewport around to get a better view and you should now see something like the following image

You will notice in this image that you don’t see any lightmaps in the opengl viewport. OpenGL in Cinema4d does not yet do any mixing of textures. To see what the end result with the lightmaps blended will look like choose Render->RenderView and you will see something like the following…

To see how this is setup we first look at the objects tags…

Each Polygon Object that is loaded in from Irrlicht has 2 Texture tags, 2 UV tags and 1 Normals Tag. These are loaded in in a particular order, that being the first Diffuse Color texture tag, then the UVs that should be used for that texture tag are next to it. After that we have the Lightmap texture tag followed by its UVs and finally the Normals Tag. If you click once on the Lightmap texture tag you will then see the following in the Attribute panel in Cinema4D.

The important thing to notice here is that the Mix Textures tag has been turned on. This will mix this texture with the previous one (that being the Diffuse Color). Now double click on the Lightmap texture tag and you will see the following in the Attributes panel…

Here you will see the Mix Mode for this texture. It is currently set to Normal. Other options are Add, Subtract and Multiply. I found that Normal gives the closest result to the what is rendered in Irrlicht (which we will come to next).
View the Level in Irrlicht
Now that we have the level loaded into Cinema4D you could happily start changing the textures, painting things, adding or removing geometry. But eventually you will want to see what its going to look like inside of Irrlicht. To do first select everything in the cinema4D viewport by pressing Cntrl-A. With everything selected go to the Plugins menu and choose “IrrlichtViewer->Run Game”. A new window will appear with the level loaded into a Irrlicht scene. You can fly around the scene using your mouse and the Arrow keys on your keyboard. When you are finished press the ESC key to close the window. It should look something like the following…

Now thats about all there is to show at the moment. You can also create any other polygonal object and add a material with a diffuse texture and just select that and through to be viewed in Irrlicht as well.
Now we move onto how this was actually all done. There are 2 parts to this, those being the Loader and the RunGame commands. First we will look at the loader.
The LoadQuake Command
Firstly there is the command itself, which is not that much. This just has an execute method and of course it is registered in the main.cpp file.
class LoadQuakeCommandData : public CommandData
{
public:
virtual Bool Execute(BaseDocument* doc);
};
Bool RegisterQuakeLoaderCommandData()
{
//Note that I should really get another ID from the maxon plugin cafe here rather than go + 1
return RegisterCommandPlugin(ID_RUNGAME_CMD+1, "LoadQuake", 0, "", "LoadQuake", gNew LoadQuakeCommandData());
}
The Execute method is where all the exciting stuff happens. Firstly this creates an IrrlichtDevice, sets it up loads in the Quake pk3 file from the media directory located in the irrlichtviewers plugin folder.
Bool LoadQuakeCommandData::Execute(BaseDocument* doc)
{
IrrlichtDevice *device = createDevice( video::EDT_NULL, dimension2d(640, 480), 16, false, false, false);
if (!device) return FALSE;
IVideoDriver* driver = device->getVideoDriver();
ISceneManager* smgr = device->getSceneManager();
gui::IGUIEnvironment* gui = device->getGUIEnvironment();
//add our private media directory to the file system
Filename fileName = GeGetPluginPath() + Filename("media");
char path[1024];
fileName.GetString().GetCString(path,1024);
device->getFileSystem()->addFolderFileArchive(path);
/*
To display the Quake 3 map, we first need to load it. Quake 3 maps
are packed into .pk3 files, which are nothing other than .zip files.
So we add the .pk3 file to our FileSystem. After it was added,
we are able to read from the files in that archive as they would
directly be stored on disk.
*/
device->getFileSystem()->addZipFileArchive ( "map-20kdm2.pk3" );
scene::IQ3LevelMesh* mesh = (scene::IQ3LevelMesh*) smgr->getMesh("maps/20kdm2.bsp");
Now for each piece of geometry in the mesh we create a new polygonal object in Cinema4D and create a normals tag for it.
scene::ISceneNode* node = 0;
if ( mesh )
{
scene::IMesh *geometry = mesh->getMesh(quake3::E_Q3_MESH_GEOMETRY);
u32 geom;
for(geom=0;geomgetMeshBufferCount();geom++)
{
IMeshBuffer* buffer = geometry->getMeshBuffer(geom);
u32 vertcount = buffer->getVertexCount();
u32 indexCount = buffer->getIndexCount();
//Find the polygon count
u32 polyCount = indexCount / 3;
//Create the polygon object
PolygonObject *newPolyObj = (PolygonObject *)BaseObject::Alloc(Opolygon);
newPolyObj->ResizeObject(vertcount,polyCount);
Vector *newPoints = newPolyObj->GetPointW();
CPolygon *newPolys = newPolyObj->GetPolygonW();
//Create a normals tag
VariableTag *nrmtag = NULL;
SWORD *normalData = NULL;
nrmtag = newPolyObj->MakeVariableTag(Tnormal,polyCount);
if(nrmtag)
{
normalData = (SWORD*)nrmtag->GetDataAddressW();
}
Now we load in all the materials and textures into Cinema4D. One thing to note here is that all the textures are created in memory. Nothing is ever saved to disk during the entire Loading of the Quake level to Running it in the IrrlichtViewer.
//Create materials and textures
if(buffer)
{
UVWTag *uvwA = NULL;
UVWTag *uvwB = NULL;
Vector textureTrans;
Vector textureRot;
Vector textureScale;
video::SMaterial &material = buffer->getMaterial();
LONG matCount=0;
for(matCount=MATERIAL_MAX_TEXTURES-1;matCount>=0;matCount--)
{
video::ITexture *pTexture = material.getTexture(matCount);
if(pTexture)
{
//Create a new material for each texture since they have different UVs. Mix the materials together using the "Mix Texture" parameter on a TextureTag
Material *newMat = Material::Alloc();
doc->InsertMaterial(newMat);
if(matCount==0)
{
//The first material will use the UVs from the the first UVTag.
uvwA = (UVWTag*)newPolyObj->MakeVariableTag(Tuvw,polyCount);
}
else if(matCount == 1 && buffer->getVertexType() == EVT_2TCOORDS)
{
//The second material will use the UVs from the second UVTag
uvwB = (UVWTag*)newPolyObj->MakeVariableTag(Tuvw,polyCount);
}
dimension2d size = pTexture->getSize();
const stringc &name = pTexture->getName();
//Set the data for the texture
BaseContainer bc;
bc.SetLong(TEXTURE_FILEFORMAT,FILTER_BMP);
bc.SetLong(TEXTURE_WIDTH, size.Width);
bc.SetLong(TEXTURE_HEIGHT, size.Height);
bc.SetLong(TEXTURE_MODE, MODE_RGB);
//Create a new material in Cinema4D
newMat->SetChannelState(CHANNEL_COLOR,TRUE);
newMat->SetName(name.c_str());
//Get the color channel for the new material
BaseChannel* colorChannel = newMat->GetChannel(CHANNEL_COLOR);
//Assign the texture name to the material so the material can reference the image.
BaseContainer bla;
bla.SetString(BASECHANNEL_TEXTURE,name.c_str());
colorChannel->SetData(bla);
//See if we have already loaded this texture
PaintTexture* newTexture = NULL;
GeListHead *head = PaintTexture::GetPaintTextureHead();
if(head)
{
PaintTexture *headTexture = (PaintTexture *)head->GetFirst();
while(headTexture)
{
if(headTexture->GetFilename() == Filename(name.c_str()))
{
newTexture = headTexture;
break;
}
headTexture = (PaintTexture *)headTexture->GetNext();
}
}
//If no texture was previously loaded then create a new one
if(!newTexture)
{
newTexture = PaintTexture::CreateNewTexture(name.c_str(), bc);
core::matrix4& textMatrix = material.getTextureMatrix(matCount);
vector3df trans = textMatrix.getTranslation();
vector3df rot = textMatrix.getRotationDegrees();
vector3df scale = textMatrix.getScale();
textureTrans.x = trans.X;
textureTrans.y = trans.Y;
textureTrans.z = trans.Z;
textureRot.x = Rad(Real(rot.X));
textureRot.y = Rad(Real(rot.Y));
textureRot.z = Rad(Real(rot.Z));
textureScale.x = scale.X;
textureScale.y = scale.Y;
textureScale.z = scale.Z;
u32 bytesPerRow = pTexture->getPitch();
ECOLOR_FORMAT format = pTexture->getColorFormat();
UCHAR *pixelData = (UCHAR *)pTexture->lock(true);
IImage* tempImage = driver->createImageFromData(format,size,pixelData,true,false); //Use the pointer for the data but don't delete it when the image is dropped
//Get the first layer for the new image
PaintLayerBmp *paintBmp = NULL;
PaintLayer *layer = newTexture->GetFirstLayer();
paintBmp = (PaintLayerBmp*)layer;
paintBmp->SetName(name.c_str());
//Write the pixels from the Irrlicht image to the C4D PaintLayerBmp;
u32 x,y;
for(x=0;x<(u32)size.Width;x++)
{
for(y=0;y<(u32)size.Height;y++)
{
video::SColor col = tempImage->getPixel(x,y);
PIX buffer[3];
buffer[0] = (UCHAR)col.getRed();
buffer[1] = (UCHAR)col.getGreen();
buffer[2] = (UCHAR)col.getBlue();
paintBmp->SetPixelCnt(x,y,1,buffer,3,MODE_RGB,0);
}
}
pTexture->unlock();
}
This is where the Mix Flag is set for the texture Tag so that the light maps display correctly when the image is rendered.
//Create the texture tag and add it to the polygon object.
//Tell the texture to MIX the color if it has a lightmap (which is the secound material in our case)
{
TextureTag *texTag = (TextureTag *)newPolyObj->MakeTag(Ttexture);
texTag->SetMaterial(newMat);
//Have a look in the file ttexture.h for a list of other flags
//C:\Program Files\MAXON\CINEMA 4D R11.5\resource\res\description\ttexture.h
BaseContainer texTagData;
texTagData.SetReal(TEXTURETAG_SIDE,TEXTURETAG_SIDE_FRONTANDBACK);
texTagData.SetBool(TEXTURETAG_TILE,TRUE);
texTagData.SetVector(TEXTURETAG_POSITION,textureTrans);
texTagData.SetVector(TEXTURETAG_SIZE,textureScale);
texTagData.SetVector(TEXTURETAG_ROTATION,textureRot);
texTagData.SetLong(TEXTURETAG_PROJECTION, TEXTURETAG_PROJECTION_UVW);
if(matCount==1)
{
texTagData.SetBool(TEXTURETAG_MIX,TRUE);
}
texTag->SetData(texTagData);
}
//Tell the material to update everything
newMat->Message(MSG_UPDATE);
newMat->Update(TRUE, TRUE);
}
}
After that we load in all the Vertices for the geometry and copy then over to the Cinema4D polygonal Object
//Copy across the vertex data to the polygon object.
if(buffer->getVertexType()==EVT_STANDARD)
{
S3DVertex *vertices = (S3DVertex*)buffer->getVertices();
u32 vertIndex;
for(vertIndex=0;vertIndex
{
const S3DVertex &theVert = vertices[vertIndex];
newPoints[vertIndex].x = theVert.Pos.X;
newPoints[vertIndex].y = theVert.Pos.Y;
newPoints[vertIndex].z = theVert.Pos.Z;
}
}
else if(buffer->getVertexType()==EVT_2TCOORDS)
{
S3DVertex2TCoords *vertices = (S3DVertex2TCoords*)buffer->getVertices();
u32 vertIndex;
for(vertIndex=0;vertIndex
{
const S3DVertex2TCoords &theVert = vertices[vertIndex];
newPoints[vertIndex].x = theVert.Pos.X;
newPoints[vertIndex].y = theVert.Pos.Y;
newPoints[vertIndex].z = theVert.Pos.Z;
}
}
else if(buffer->getVertexType()==EVT_TANGENTS)
{
S3DVertexTangents *vertices = (S3DVertexTangents*)buffer->getVertices();
u32 vertIndex;
for(vertIndex=0;vertIndex
{
const S3DVertexTangents &theVert = vertices[vertIndex];
newPoints[vertIndex].x = theVert.Pos.X;
newPoints[vertIndex].y = theVert.Pos.Y;
newPoints[vertIndex].z = theVert.Pos.Z;
}
}
And lastly we create the UVs and Normals for the polygonal objects
//Set the normals and UVs for the polygon object
switch(buffer->getIndexType())
{
case video::EIT_16BIT:
{
u16 *indices = (u16 *)buffer->getIndices();
u32 index;
u32 polyCount = 0;
for(index=0;index
{
newPolys[polyCount].a = indices[index];
newPolys[polyCount].b = indices[index+1];
newPolys[polyCount].d = newPolys[polyCount].c = indices[index+2];
//If its the standard type then we need to cast to S3DVertex
if(buffer->getVertexType()== EVT_STANDARD && uvwA)
{
S3DVertex *vertices = (S3DVertex*)buffer->getVertices();
UVWStruct uv;
const S3DVertex &theVertA = vertices[indices[index]];
const S3DVertex &theVertB = vertices[indices[index+1]];
const S3DVertex &theVertC = vertices[indices[index+2]];
//Set the UVs for the first UV set (the diffuse color)
uv.a.x = theVertA.TCoords.X;
uv.a.y = theVertA.TCoords.Y;
uv.b.x = theVertB.TCoords.X;
uv.b.y = theVertB.TCoords.Y;
uv.c.x = theVertC.TCoords.X;
uv.c.y = theVertC.TCoords.Y;
uvwA->SetSlow(polyCount,uv);
//Set the normals
if(normalData)
{
StoreNormal(normalData+12*polyCount+0,Vector(theVertA.Normal.X,theVertA.Normal.Y,theVertA.Normal.Z));
StoreNormal(normalData+12*polyCount+3,Vector(theVertB.Normal.X,theVertB.Normal.Y,theVertB.Normal.Z));
StoreNormal(normalData+12*polyCount+6,Vector(theVertC.Normal.X,theVertC.Normal.Y,theVertC.Normal.Z));
StoreNormal(normalData+12*polyCount+9,Vector(theVertC.Normal.X,theVertC.Normal.Y,theVertC.Normal.Z));
}
}
//If its the standard type then we need to cast to S3DVertex
else if(buffer->getVertexType()==EVT_2TCOORDS)
{
UVWStruct uv;
S3DVertex2TCoords *vertices = (S3DVertex2TCoords*)buffer->getVertices();
const S3DVertex2TCoords &theVertA = vertices[indices[index]];
const S3DVertex2TCoords &theVertB = vertices[indices[index+1]];
const S3DVertex2TCoords &theVertC = vertices[indices[index+2]];
//UV A (used by the Diffuse Color texture)
if(uvwA)
{
uv.a.x = theVertA.TCoords.X;
uv.a.y = theVertA.TCoords.Y;
uv.b.x = theVertB.TCoords.X;
uv.b.y = theVertB.TCoords.Y;
uv.c.x = theVertC.TCoords.X;
uv.c.y = theVertC.TCoords.Y;
uvwA->SetSlow(polyCount,uv);
}
//UV B (used byt the Light map texture)
if(uvwB)
{
uv.a.x = theVertA.TCoords2.X;
uv.a.y = theVertA.TCoords2.Y;
uv.b.x = theVertB.TCoords2.X;
uv.b.y = theVertB.TCoords2.Y;
uv.c.x = theVertC.TCoords2.X;
uv.c.y = theVertC.TCoords2.Y;
uvwB->SetSlow(polyCount,uv);
}
//Set the normals
if(normalData)
{
StoreNormal(normalData+12*polyCount+0,Vector(theVertA.Normal.X,theVertA.Normal.Y,theVertA.Normal.Z));
StoreNormal(normalData+12*polyCount+3,Vector(theVertB.Normal.X,theVertB.Normal.Y,theVertB.Normal.Z));
StoreNormal(normalData+12*polyCount+6,Vector(theVertC.Normal.X,theVertC.Normal.Y,theVertC.Normal.Z));
StoreNormal(normalData+12*polyCount+9,Vector(theVertC.Normal.X,theVertC.Normal.Y,theVertC.Normal.Z));
}
}
polyCount++;
}
}
break;
case EIT_32BIT:
{
u32 *indices = (u32*)buffer->getIndices();
}
break;
default:
break;
}
doc->InsertObject(newPolyObj,0,0);
newPolyObj->Message(MSG_UPDATE);
}
}
}
device->drop();
EventAdd();
return TRUE;
}
Next we will look at the Irrlicht Viewer code..
Irrlicht Viewer Code
Now that we have all the data loaded the next thing we want to do is open up a Irrlicht Viewer and view the scene as it will appear in any game we might make.
Firstly there is the RunGameCommand. This is a bit different from the other load command because we want to run the game in its own thread. So to do this we will use Cinema4D thread. In this case we have created a new class called GameThread that is amember of the RunGameCommand.
//===========================================================================
// This is the command to export all the data to Irrlicht and run it in its own thread.
//===========================================================================
class RunGameCommandData : public CommandData
{
public:
virtual Bool Execute(BaseDocument* doc);
private:
GameThread gameThread;
};
Bool RunGameCommandData::Execute(BaseDocument* doc)
{
gameThread.theDocToView->SetLink(doc);
return gameThread.Start(TRUE);
}
Bool RegisterRunGameCommandData()
{
return RegisterCommandPlugin(ID_RUNGAME_CMD, "Run Game", 0, "", "Run Game", gNew RunGameCommandData());
}
The GameThread class is derived from C4DThread and also IEventReciever. C4DThread has a main method which starts the thread. The IEventReceiver is part of Irrlicht and has an OnEvent virtual method that we can use to detect the ESC key so that we can quit out of the game.
//===========================================================================
// To run the Irrlicht scene we run it in our own thread.
// This also has an IEventReciever so that the user can press the Escape key to exit the scene
//===========================================================================
class GameThread : public C4DThread, public IEventReceiver
{
public:
GameThread();
virtual ~GameThread();
virtual void Main(void);
virtual const CHAR* GetThreadName(void);
private:
virtual bool OnEvent(const SEvent& event);
void Init();
void DoRecursion(BaseObject *op);
void AddObject(BaseObject *obj);
void AddLight(BaseObject *obj);
void Run();
public:
AutoAlloc theDocToView;
private:
void ViewObject(BaseObject *obj);
IrrlichtDevice *device;
};
The main Method loops over all the selected object in the cinema4d scene, using a special recursive method, and adds all the polygonal objects to an Irrlicht scene.
// This method finds all selected objects in the scene and exports then to Irrlicht
void GameThread::Main(void)
{
Init();
BaseDocument *doc = (BaseDocument *)theDocToView->ForceGetLink();
AutoAlloc selection;
doc->GetActiveObjects(selection,false);
LONG a;
Bool found = FALSE;
for(a=0;aGetCount();a++)
{
C4DAtom* atom = selection->GetIndex(a);
if(atom->IsInstanceOf(Obase))
{
DoRecursion((BaseObject*)atom);
}
else if(atom->IsInstanceOf(Olight))
{
BaseObject *obj = (BaseObject *)atom;
AddLight(obj);
}
}
Run(); //Finally run the scene
}
void GameThread::DoRecursion(BaseObject *op)
{
BaseObject *tp = op->GetDeformCache();
if (tp)
{
DoRecursion(tp);
}
else
{
tp = op->GetCache(NULL);
if (tp)
{
DoRecursion(tp);
}
else
{
if (!op->GetBit(BIT_CONTROLOBJECT))
{
if (op->IsInstanceOf(Opolygon))
{
AddObject(op);
}
}
}
}
for (tp = op->GetDown(); tp; tp=tp->GetNext())
{
DoRecursion(tp);
}
}
AddObject is where all the magic happens for this command. First we get the polygon object from Cinema4D, check how many UV Tags it has and then create a mesh in Irrlicht
//Add a polygonal object to the scene including multiple UV sets, materials and textures.
void GameThread::AddObject(BaseObject *obj)
{
if(!obj) return;
LONG a;
//Find the polyon object and the uv sets if they exist
BaseDocument *doc = GetActiveDocument();
PolygonObject *pPoly = ToPoly(obj);
UVWTag *uvwA = (UVWTag *)pPoly->GetTag(Tuvw,0);
UVWTag *uvwB = (UVWTag *)pPoly->GetTag(Tuvw,1);
//Create a mesh in Irrlicht
ISceneManager* smgr = device->getSceneManager();
IVideoDriver* driver = device->getVideoDriver();
scene::SMesh *mesh = new scene::SMesh();
mesh->setMaterialFlag(EMF_BACK_FACE_CULLING,FALSE);
If we have two UV sets then we need to create a scene::SMeshBufferLightMap object to add to the irrlicht scene. To begin with we then count the number of triangles in the scene. Since Cinema4D has quads as well as triangles we check to see if it shares indices, if it does that means its only a 3 sided triangle. Later on if we found any quads we cut them up and add then as triangles to the scene.
//If there are 2 uv sets then we should have 2 materials and 2 textures. The first being the diffuse color and the second the light map
if(uvwA && uvwB)
{
scene::SMeshBufferLightMap *sbuffer = new scene::SMeshBufferLightMap();
const Vector *pPoints = pPoly->GetPointR();
//Count the number of triangles in the scene. Cinema4D also supports quads so we need to cut these into triangles when we export to Irrlicht
LONG count = 0;
const CPolygon *pPolys = pPoly->GetPolygonR();
for(a=0;aGetPolygonCount();++a)
{
const CPolygon &poly = pPolys[a];
if(poly.c == poly.d)
{
count += 3;
}
else
{
count += 4;
}
}
sbuffer->Indices.reallocate(count);
sbuffer->Vertices.reallocate(count);
//Get the normal tag if it exists. If it doesn't exist we will create normals ourselves
VariableTag *nrmtag = (VariableTag*)pPoly->GetTag(Tnormal);
SWORD *normalData = NULL;
if(nrmtag)
{
normalData = (SWORD*)nrmtag->GetDataAddressW();
}
Vector normalA, normalB, normalC, normalD;
Now we add all the polygons to the scene, making sure to cut up any quads into triangles as we do so. We will also set the normals uvs for the polygons in the scene.
//Add all the triangles to the scene
u16 vertCount = 0;
for(a=0;aGetPolygonCount();++a)
{
const CPolygon &poly = pPolys[a];
UVWStruct uvsA;
if(uvwA)
{
uvsA = uvwA->GetSlow(a);
}
UVWStruct uvsB;
if(uvwB)
{
uvsB = uvwB->GetSlow(a);
}
if(normalData)
{
GetNormal(normalData+12*a+0,normalA);
GetNormal(normalData+12*a+3,normalB);
GetNormal(normalData+12*a+6,normalC);
GetNormal(normalData+12*a+9,normalD);
}
sbuffer->Indices.push_back(vertCount++);
sbuffer->Indices.push_back(vertCount++);
sbuffer->Indices.push_back(vertCount++);
//If no normals then create some
if(!normalData)
{
Vector normal = (pPoints[poly.b] - pPoints[poly.a]) %(pPoints[poly.c] - pPoints[poly.a]);
normal.Normalize();
normalA = normalB = normalC = normal;
}
sbuffer->Vertices.push_back(video::S3DVertex2TCoords(pPoints[poly.a].x, pPoints[poly.a].y, pPoints[poly.a].z,normalA.x,normalA.y,normalA.z,video::SColor(255,255,255,255),uvsA.a.x,uvsA.a.y,uvsB.a.x,uvsB.a.y));
sbuffer->Vertices.push_back(video::S3DVertex2TCoords(pPoints[poly.b].x, pPoints[poly.b].y, pPoints[poly.b].z,normalB.x,normalB.y,normalB.z,video::SColor(255,255,255,255),uvsA.b.x,uvsA.b.y,uvsB.b.x,uvsB.b.y));
sbuffer->Vertices.push_back(video::S3DVertex2TCoords(pPoints[poly.c].x, pPoints[poly.c].y, pPoints[poly.c].z,normalC.x,normalC.y,normalC.z,video::SColor(255,255,255,255),uvsA.c.x,uvsA.c.y,uvsB.c.x,uvsB.c.y));
//This is a quad so we need to export a second triangle
if(poly.c != poly.d)
{
//If no normals then create some
if(!normalData)
{
Vector normal = (pPoints[poly.c] - pPoints[poly.a]) %(pPoints[poly.d] - pPoints[poly.a]);
normal.Normalize();
normalA = normalC = normalD = normal;
}
sbuffer->Indices.push_back(vertCount++);
sbuffer->Indices.push_back(vertCount++);
sbuffer->Indices.push_back(vertCount++);
sbuffer->Vertices.push_back(video::S3DVertex2TCoords(pPoints[poly.a].x, pPoints[poly.a].y, pPoints[poly.a].z,normalA.x,normalA.y,normalA.z,video::SColor(255,255,255,255),uvsA.a.x,uvsA.a.y,uvsB.a.x,uvsB.a.y));
sbuffer->Vertices.push_back(video::S3DVertex2TCoords(pPoints[poly.c].x, pPoints[poly.c].y, pPoints[poly.c].z,normalC.x,normalC.y,normalC.z,video::SColor(255,255,255,255),uvsA.c.x,uvsA.c.y,uvsB.c.x,uvsB.c.y));
sbuffer->Vertices.push_back(video::S3DVertex2TCoords(pPoints[poly.d].x, pPoints[poly.d].y, pPoints[poly.d].z,normalD.x,normalD.y,normalD.z,video::SColor(255,255,255,255),uvsA.d.x,uvsA.d.y,uvsB.d.x,uvsB.d.y));
}
}
sbuffer->recalculateBoundingBox();
mesh->addMeshBuffer(sbuffer);
sbuffer->drop();
}
If there was only 1 UV tag on the polygon object then we need to do things slightly differently, but the code is almost exactly the same as what was given above.
else if(uvwA && !uvwB) //No second UV Set. Virtually the same code as above.
{
scene::SMeshBuffer *sbuffer = new scene::SMeshBuffer();
const Vector *pPoints = pPoly->GetPointR();
LONG count = 0;
const CPolygon *pPolys = pPoly->GetPolygonR();
for(a=0;aGetPolygonCount();++a)
{
const CPolygon &poly = pPolys[a];
if(poly.c == poly.d)
{
count += 3;
}
else
{
count += 4;
}
}
sbuffer->Indices.reallocate(count);
sbuffer->Vertices.reallocate(count);
VariableTag *nrmtag = (VariableTag*)pPoly->GetTag(Tnormal);
SWORD *normalData = NULL;
if(nrmtag)
{
normalData = (SWORD*)nrmtag->GetDataAddressW();
}
Vector normalA, normalB, normalC, normalD;
u16 vertCount = 0;
for(a=0;aGetPolygonCount();++a)
{
const CPolygon &poly = pPolys[a];
UVWStruct uvsA;
if(uvwA)
{
uvsA = uvwA->GetSlow(a);
}
UVWStruct uvsB;
if(uvwB)
{
uvsB = uvwB->GetSlow(a);
}
if(normalData)
{
GetNormal(normalData+12*a+0,normalA);
GetNormal(normalData+12*a+3,normalB);
GetNormal(normalData+12*a+6,normalC);
GetNormal(normalData+12*a+9,normalD);
}
sbuffer->Indices.push_back(vertCount++);
sbuffer->Indices.push_back(vertCount++);
sbuffer->Indices.push_back(vertCount++);
if(!normalData)
{
Vector normal = (pPoints[poly.b] - pPoints[poly.a]) %(pPoints[poly.c] - pPoints[poly.a]);
normal.Normalize();
normalA = normalB = normalC = normal;
}
sbuffer->Vertices.push_back(video::S3DVertex(pPoints[poly.a].x, pPoints[poly.a].y, pPoints[poly.a].z,normalA.x,normalA.y,normalA.z,video::SColor(255,255,255,255),uvsA.a.x,uvsA.a.y));
sbuffer->Vertices.push_back(video::S3DVertex(pPoints[poly.b].x, pPoints[poly.b].y, pPoints[poly.b].z,normalB.x,normalB.y,normalB.z,video::SColor(255,255,255,255),uvsA.b.x,uvsA.b.y));
sbuffer->Vertices.push_back(video::S3DVertex(pPoints[poly.c].x, pPoints[poly.c].y, pPoints[poly.c].z,normalC.x,normalC.y,normalC.z,video::SColor(255,255,255,255),uvsA.c.x,uvsA.c.y));
if(poly.c != poly.d)
{
if(!normalData)
{
Vector normal = (pPoints[poly.c] - pPoints[poly.a]) %(pPoints[poly.d] - pPoints[poly.a]);
normal.Normalize();
normalA = normalC = normalD = normal;
}
sbuffer->Indices.push_back(vertCount++);
sbuffer->Indices.push_back(vertCount++);
sbuffer->Indices.push_back(vertCount++);
sbuffer->Vertices.push_back(video::S3DVertex(pPoints[poly.a].x, pPoints[poly.a].y, pPoints[poly.a].z,normalA.x,normalA.y,normalA.z,video::SColor(255,255,255,255),uvsA.a.x,uvsA.a.y));
sbuffer->Vertices.push_back(video::S3DVertex(pPoints[poly.c].x, pPoints[poly.c].y, pPoints[poly.c].z,normalC.x,normalC.y,normalC.z,video::SColor(255,255,255,255),uvsA.c.x,uvsA.c.y));
sbuffer->Vertices.push_back(video::S3DVertex(pPoints[poly.d].x, pPoints[poly.d].y, pPoints[poly.d].z,normalD.x,normalD.y,normalD.z,video::SColor(255,255,255,255),uvsA.d.x,uvsA.d.y));
}
}
sbuffer->recalculateBoundingBox();
mesh->addMeshBuffer(sbuffer);
sbuffer->drop();
}
Now we setup the mesh as a scenenode and add it to scene manager in Irrlicht
mesh->recalculateBoundingBox();
//Set the position of the mesh in the scene
IMeshSceneNode* node = smgr->addMeshSceneNode( mesh );
Vector pos = obj->GetPos() * obj->GetUpMg(); //Location in world coordinates
node->setPosition(vector3df(pos.x,pos.y,pos.z));
Vector rotation = obj->GetRot() * obj->GetUpMg();
node->setRotation(vector3df(Deg(rotation.x),Deg(rotation.y),Deg(rotation.z)));
Vector scale = obj->GetScale() * obj->GetUpMg();
node->setScale(vector3df(scale.x,scale.y,scale.z));
//Set its material type if it has a light map or not
if(uvwA && !uvwB)
{
node->setMaterialType(video::EMT_SOLID);
}
else if(uvwA && uvwB)
{
node->setMaterialType(video::EMT_LIGHTMAP_M4);
}
Then finally we copy across all the textures. Note that I am not checking if a texture has already been used in the Irrlicht scene so this is currently wasting more memory than is required.
if (node)
{
Filename docpath = GetActiveDocument()->GetDocumentPath();
LONG textureCount = 0;
TextureTag *tex=(TextureTag*)obj->GetTag(Ttexture,textureCount);
while(tex)
{
BaseMaterial *mat = tex->GetMaterial();
if(mat)
{
BaseChannel* diffuseColor = mat->GetChannel(CHANNEL_COLOR);
if(diffuseColor)
{
PaintTexture *theTexture = PaintTexture::GetPaintTextureOfBaseChannel(doc,diffuseColor);
if(theTexture)
{
PaintLayer *layer = theTexture->GetFirstLayer();
PaintLayerBmp *paintBmp = (PaintLayerBmp*)layer;
video::IImage *image = driver->createImage(video::ECF_R8G8B8,core::dimension2di(paintBmp->GetBw(),paintBmp->GetBh()));
LONG x,y;
for(x=0;xGetBw();x++)
{
for(y=0;yGetBh();y++)
{
PIX buffer[3];
paintBmp->GetPixelCnt(x,y,1,buffer,MODE_RGB,0);
image->setPixel(x,y,video::SColor(255,buffer[0],buffer[1],buffer[2]));
}
}
video::ITexture *newTex = driver->addTexture(mat->GetName().GetCStringCopy(),image);
node->setMaterialTexture(textureCount,newTex);
}
}
}
tex=(TextureTag*)obj->GetTag(Ttexture,++textureCount);
}
}
}
There is also a method to Initialize the Irrlicht scene
//Initialize Irrlicht
void GameThread::Init()
{
device = createDevice( video::EDT_OPENGL, dimension2d(640, 480), 16, false, false, false, this);
if (!device) return;
device->setWindowCaption(L"Cinema4D Irrlicht Demo");
IVideoDriver* driver = device->getVideoDriver();
ISceneManager* smgr = device->getSceneManager();
//Turn off all Clipping
for(int a=0; a < 6; a++)
{
driver->enableClipPlane(a,false);
}
scene::ICameraSceneNode *cameraNode = smgr->addCameraSceneNodeFPS();
cameraNode->setTarget(vector3df(0,0,0));
cameraNode->setPosition(vector3df(200,200,200));
smgr->setAmbientLight(video::SColorf(1.0,1.0,1.0));
device->getCursorControl()->setVisible(false);
}
And finally running the game and the OnEvent callback
//Run the game
void GameThread::Run()
{
if(!device) return;
IVideoDriver* driver = device->getVideoDriver();
ISceneManager* smgr = device->getSceneManager();
IGUIEnvironment* guienv = device->getGUIEnvironment();
while(device->run())
{
if (device->isWindowActive())
{
driver->beginScene(true, true, SColor(255,100,101,140));
smgr->drawAll();
guienv->drawAll();
driver->endScene();
}
else
{
device->yield();
}
device->sleep(1);
}
device->drop();
}
//Check for the escape key to exit the "game"
bool GameThread::OnEvent(const SEvent& event)
{
if (event.EventType == EET_KEY_INPUT_EVENT && event.KeyInput.PressedDown == false)
{
if (event.KeyInput.Key == irr::KEY_ESCAPE)
{
if (device)
{
device->closeDevice();
return true;
}
}
}
return false;
}
const CHAR* GameThread::GetThreadName(void)
{
return "GameThread";
}
There are a couple of other methods that are used to Get and Set the normals for the geometry. These are very specific to Cinema4D.
//Some special code to set and get normals from Cinema4D.
#define normDiv 1.0/32000.0
inline static void GetNormal(SWORD *ptr, Vector &v)
{
v.x = SWORD(ptr[0]*normDiv);
v.y = SWORD(ptr[1]*normDiv);
v.z = SWORD(ptr[2]*normDiv);
}
inline static void StoreNormal(SWORD *ptr, Vector v)
{
ptr[0] = SWORD(v.x*32000.0);
ptr[1] = SWORD(v.y*32000.0);
ptr[2] = SWORD(v.z*32000.0);
}
And thats all there is to it!
Conclusion
In this tutorial you learned how to load in a Quake Level using Irrlicht to load in the geometry and then transfer it through to Cinema4D. You also learned how to take the geometry from Cinema4D and export that out again to be viewed in the Irrlicht Viewer. This does not only work with Quake levels though, you can use this to export anything from Cinema4D to be viewed in Irrlicht. And with a few slight changes you could even save out the Irrlicht scene to disk and use that in a stand alone game if you so wished.
Where to from here?
You could build upon this system and add some of your own Tags to Cinema4D. For example you could add a Tag that would tell this exporter that the polygon object is actually the starting location for the player character, then when the game is run the player starts in a more reasonable place. You could then simply insert one of the standard Quake Characters and run around the level.
You could add more tags for things like Monster locations, triggers, treasures etc… and add them to this exporter to populate your little game with some extra data.
You could setup your level in Cinema4D, preview the game and then export to some custom binary file and use it in a stand alone game engine.