Importing Quake files and previewing levels using Irrlicht (Part 3)
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
//===========================================================================
// 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 saw how to load in a Quake Level using Irrlicht to load in the geometry and then transfer it throught to Cinema4D. You also saw 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 apon 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 may see where I am going with this, and I will add more tutorials about this in future, but the idea I am trying to get at here is to use Cinema4D as your Level Editor. You can setup your level in Cinema4D, preview the game and when its how you want then export to some custom binary file and use it in a stand alone game engine.
Well I hope you found this tutorial somewhat interesting and hope to see you back here again sometime soon!
Comments
Post a Comment