CONTENT

Home
About

My Projects
DirectX Demos
Games
3D Engine
Screensavers
Java Applets
Pokersniffer



Engine Introduction
Many people have asked me for the DirectX demo C++ source code, so here it is. The framework I have build for creating the demos consist of the following:

* Kernel system, with scene manager, camera and simple culling.
* Model loading (.x files) and collision detection. I've made my models with Delgine and exported to .x file.
* The usual frustum, resource, log classes etc.
* Rendering with HLSL shader.
* Cube and stencil shadows. Environment mapping onto objects.
* Settings manager for parsing simple settings from text files.
* Simple HUD elements and UI buttons.
* Emitter system for smoke and fire effects.
* Screen distort effects (haze effects etc).
* Skybox and clouds, background effekt.
* ODE physic integration so we can throw around with simple objects.
* TODO: Skinning animations, level editor, endless list ..

I do not in any way say that my design and approach is the 'golden path' to follow if you want to create your own game engine. If you know a shortcut, please take it.


Engine Demo Samples, Download:   Full source, Visual Studio 2010   Executable only (no source)


Sample 1: Cubemap shadows, environment mapping and collision detection.

This sample renders a simple scene and an omni light which cast a GPU shadow. The light source is moving, so the shadow has to be generated each frame. The brick in the center is made of a shining material, so the environment is reflected on the brick, kind of unreal, ohh well .. Also, I've added collision detection so you can trace the triangles with the crosshair.



Sample 2: Emitter system with fire/smoke effects and specular mapping.

What we have here is a simple emitter system. Fire and smoke side by side, have you ever seen that before? - well it demonstrates how to use a 3D texture for the fire and just a 2D texture for the smoke. Everything pulled through HLSL code. The scene has a simple skybox, and I've added a specular shader on the geometry, so everything shines nicely (a bit unreal .. ooh well).



Sample 3: ODE box physics, static lightmaps and stencil shadows from boxes.

It is time to get angry and throw around with some boxes. Yes, I know that box physics is the most simple physics simulation there is, but the point with this sample is to show you how to get ODE up and running. You can do a lot more with ODE, check the documentation. Push a box with the left mouse button, and pull a box with the right button. If you grab a box, then throw it with the left mouse button. I've added a shock-wave effect when you throw the box. This looks easy, but it took me about one week to get it all working (not so easy after all).





The Matrix
"Morpheus: You take the blue pill - the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill - you stay in Wonderland and I show you how deep the rabbit-hole goes". Lets take the red pill .. and create our own engine, read on ..


The Demo Solution
If you look at the source code in the DirectX SDK you will notice that all the cool demos are more or less coded in a few big classes which utilize the DX utility functions. This is a fine approach for demonstrating stuff, but if you have the healthy desire to create you own 3D engine or complex demos, I believe you are in much better shape my creating a solution with a set of projects. The single threaded framework I'm about to present is constructed this way:



The Engine project contains the core elements like camera, scene manager etc. and it also contains what we need to get DirectX up and running. The Game project is the startup project which holds our main.cpp where everything starts (good spot for a break point). The library is just a collection of standalone classes like vector3D, math stuff and so on. The ODE facade goes into the Physic project, and all shaders are collected in the Shader project. I've extended this solution with a level editor project which uses the engine to build a scene in a what-you-see is what-you-get kind of way. But the editor implementation is a bit buggy at the moment, so I've removed it for now. You may or may not like this approach, please tweak it in any way you like.

Initializing
The first step is to create a window, then initialize DirectX, load up our framework and if everything loads with success, we can start the main loop. This all takes place in main.cpp in the Game project. The creation of a window and setup of DirectX is well documented in the DirectX SDK, so I will skip the details and just show you the flow here:

:

hwnd = CreateWindowEx(NULL,"UGPClass","XDemo", ...);

if(!hwnd) return 0; // check if win created ok

ShowWindow(hwnd, SW_SHOW); // show the window

UpdateWindow(hwnd); // update display

 

// init the directx device

if(!CRenderDevice::GetSingleton().Initialize(hwnd, ...)) return 0;

// make our singletons

CreateSingletons();

// initialize the game/demo tasks

if (!InitAllTasks()) return 0;

 

// main process loop

while (1)

{

  if (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE))

  {

    if(msg.message == WM_QUIT)

      break; // leave if we hit Esc

 

    TranslateMessage(&msg);

    DispatchMessage(&msg);

  }

  else

  {

    // tick the timer

    UpdateTime();

    // process and render a single frame

    Process();

    // (and more)

    :

  }

}

// cleanup ..

:

The Kernel
All objects which will have to be processed each frame in the main loop will be stored in the kernel. This goes from scene objects to input processing and the updating of camera position, frustrum etc. Now, these objects have a lot of names like entities or elements. Whatever you want to call you collection of stuff that has to be processed, rendered or updated each frame, they all have one thing in common. They go into the kernel as tasks. Each task in the kernel has a priority and can be started and stopped, or suspended and resumed. For a small demo, this may seems like overkill, but still, it is flexible approach for controlling what to process and what to skip. For example, in a larger demo (small game) the rendering of a lens-flare effect is most likely suspended when the camera in inside a solid building, or we may have different sounds objects (background sound etc.) which are stopped deepening on the situation. Small demos will just have all tasks processed each frame.

void CKernel::Render()

{

  for(list<ITask*>::const_iterator it=taskList.begin(); it!=taskList.end();)

  {

    if ( (*it)->IsRunning())  //only running tasks gets rendered

      (*it)->Render();

    ++it;

  }

}

Also, the kernel has similar methods for updating of the logic, called tick, and stuff which must be done just before and after rendering called PreRender and PostRender respectively. For example, a fire/smoke effect will have the position of quads calculated in the tick method and the actual drawing will go into to rendering method. The idea behind this is to try to separate the logic from the drawing. If your project grows, this is good pratice.

Scene manager
One of the big tasks in the kernel will be the scene manager. This manager holds all solid geometry in the scene. To make this just a little bit flexible for the future I've chosen this methodology: each object which consists of triangles to be rendered is called an actor. Each actor has a location and rotation, and it has a model. The model holds the material information, triangles for collision tests and a mesh. When an actor is rendered it is the mesh of the model for that actor which is rendered. Also each actor has a list of light sources which it touches. An actor can be static (building) or dynamic (physic box). The mesh is loaded from and .x file, but could be any other format. One of the benefits by using .x files is that we get a lot of DirectX function to help us out. For example it's easy to create the TBN matrix needed for normal-mapping and we get hardware optimized drawing with index buffers and all that. This is what it looks like when we load a model:

bool CModel::Load(const char *filename, float scale)

{

  // support .x files only

  string mesh = filename;

  if (mesh.find(".x",0)!= std::string::npos)

  {

    pMesh = new CXMesh();

    if ( !pMesh->Load( mesh.c_str(), RESOURCE.Model()->pRender, scale) )

      return false;

  }

  else

  {

    LOG(ERRORBOX, "Model format not supported: %s", filename);

    return false;

  }

 

  // set mesh rotation and location

  pMesh->SetRotation(yaw, pitch, roll);

  pMesh->SetTranslation(vLocation);

  pMesh->CalculateAABB(aabb);

  // create collision instance for this model

  pCollision = new CCollision();

  pMesh->SetCollision( pCollision );

  // set the center for this model

  vCenter = pMesh->vCenter;

  // bound box for this model

  vBBoxMax = pMesh->vBBoxMax;

  vBBoxMin = pMesh->vBBoxMin;

  Radius = pMesh->Radius;

  // update matrixes

  D3DXMatrixIdentity(&matTrans);

  D3DXMatrixTranslation(&matTrans, vLocation.x, vLocation.y, vLocation.z);

  D3DXMatrixIdentity(&matRot);

  D3DXMatrixRotationYawPitchRoll(&matRot, yaw, pitch, roll);

   :

  return true;

}

 



When a model is rendered, we loop through all the light sources which touch the model and invoked the shader assigned to it for each light source. Typically, a model will have an ambient shader (background light) and omni/spot light which it also touches. The omni shader for the model can be tweaked in any way you like, it all depends on how you want the model to appear. The most common omni shader is the diffuse.fx shader which just creates a standard phong look. The code for drawing a model goes like this:

void CModel::Render()

{

  :

  RESOURCE.Model()->pRender->ChangeShader( shaderId ); // set the current shader

  RESOURCE.Model()->UpdateMatrix( shaderId );

  // loop through all light sources which touches this model

  for (int i=0; i<lightListCount; i++)

  {

    // set constants common for all light type: Omni, spot, proj, ambi

    RESOURCE.Model()->pRender->SetLightPos(lightList[i]->vPosition);

    RESOURCE.Model()->pRender->SetLightRange(lightList[i]->Range);

    RESOURCE.Model()->pRender->SetLightItensity(lightList[i]->Itensity);

    RESOURCE.Model()->pRender->SetLightColor(lightList[i]->Color/255.0f);

    RESOURCE.Model()->pRender->SetTileDiffuse(vTileDiffuse);

    RESOURCE.Model()->pRender->SetTileBump(vTileBump);

 

    // special constants goes here ..

    if ( lightList[i]->LightType == LIGHTTYPE::AMBI )

     :

    else if ( lightList[i]->LightType == LIGHTTYPE::SPOT )

     :

    else if ( lightList[i]->LightType == LIGHTTYPE::PROJ )

     :

    else if ( lightList[i]->LightType == LIGHTTYPE::OMNI )

     :

    else { continue; }  // ups

    

    // set envmap shader parameters (if any)

    if (hasEnvMap)

    {

    RESOURCE.Model()->pRender->SetConstant("EnvItensity", pEnvMap->MapItensity);

    RESOURCE.Model()->pRender->SetCube("txEnv", pEnvMap->CubeMap);

    }

 

    // Now all shader parameters for this light sources is applied on

    // the mesh. Lets draw the mesh, and go to next light source.

    pMesh->Draw( RESOURCE.Model()->pRender );

  }

  :

}



Apply Shader
This leads us the use of HLSL shaders. When you look at the code above, you can see the usage of a render pointer for a model stored inside a resource singleton. This may look complicated, but it is really not. To help us with shader calls, I've taken the approach to create a render class. This is strongly inspired by what ATI does in their SDK for developers. The idea is to have a render class where you can add a number of shaders, textures and set techniques and other constants. One technique could be spot for spot lights and so forth. A model has a render class and we add shaders and textures to that render and apply them when drawing the mesh. This is what it looks like when adding a new shader to the render class.

int CRender::AddShader(const char *file)

{

  if (shaderCount > MAX_SHADERS) return -1; // ups

  // search for effect file first before loading

  string efile = file;

  int i=0;

  for (vector<string>::iterator it=effectFile.begin(); it!=effectFile.end();)

  {

    if ( (*it) == efile )

     return i;

    i++;

    ++it;

  }

 

  // effect file not found, create new

  effectFile.push_back(efile);

  LPD3DXEFFECT newEffect=NULL;

  HRESULT hr = D3DXCreateEffectFromFile(DEVICE.GetDevice(), file, ...);

  if (hr==D3DXERR_INVALIDDATA)

  {

    LOG(ERRORBOX, "Error in loading shader: %s, D3DXERR_INVALIDDATA", file);

    return -1;

  }

  if (hr==D3DERR_INVALIDCALL)

  {

    LOG(ERRORBOX, "Error in loading shader: %s, D3DERR_INVALIDCALL", file);

    return -1;

  }

  if (hr==E_OUTOFMEMORY )

  {

    LOG(ERRORBOX, "Error in loading shader: %s, E_OUTOFMEMORY", file);

    return -1;

  }

 

  // From the DirectX documentation:

  // Instead of using strings, it would be more efficient to cache a handle to 

  // the parameter by calling ID3DXEffect::GetParameterByName(..)

  if(hr==D3D_OK)

  {

    tEffect e;

    memset(&e,0,sizeof(tEffect));

    e.effect = newEffect;

 

    // technique

    e.hAmbi = newEffect->GetTechniqueByName("lit_ambi");

    e.hOmni = newEffect->GetTechniqueByName("lit_omni");

    e.hOmniNoBump = newEffect->GetTechniqueByName("lit_omni_nobump");

    e.hSpot = newEffect->GetTechniqueByName("lit_spot");

    e.hProj = newEffect->GetTechniqueByName("lit_proj");

      :

    // matrix

    e.hMatWorld = newEffect->GetParameterByName(NULL,"matWorld");

    e.hMatWorldView = newEffect->GetParameterByName(NULL,"matWorldView");

      :

    // common constants

    e.hEyePos = newEffect->GetParameterByName(NULL,"mEyePos");

    e.hEyeLook = newEffect->GetParameterByName(NULL,"mEyeLook");

    e.hBitCastShadow = newEffect->GetParameterByName(NULL,"CastShadow");

    e.hLightPos = newEffect->GetParameterByName(NULL,"LightPos");

    e.hLightDir = newEffect->GetParameterByName(NULL,"LightDir");

    e.hLightRange = newEffect->GetParameterByName(NULL,"LightRange");

    e.hLightItensity = newEffect->GetParameterByName(NULL,"LightItensity");

    e.hLightColor = newEffect->GetParameterByName(NULL,"LightColor");

      :

    effectList.push_back(e);

    currentShaderId = shaderCount;

    shaderCount++;

  }

  else

  {

    LOG(ERRORBOX, "Unknown error in loading shader: %s", file);

    return -1;

  }

  return (shaderCount-1);

}



After the shader is added, we add textures including volume textures if needed. When the actual drawing takes place we apply a shader and set appropriate constants: world-view-proj matrix, light source location and so on. If another pass is needed we can change shader and apply that one.

Scene maps
If a model is made by a shining material, it will most likely reflect the surroundings. If you look at the model rendering above, you will notice the has-environment-map check. If a model has an environment map we pass it to the shader. The question is how the environment map is created - well in order to keep this logic in a separate place I've created a static scene map class which supply us with methods to create the needed environment maps and static light maps (plus any other maps you may need in the future)

// Generate environment for actor with id = actorId

void CSceneMaps::GenerateEnviromentMap(int actorId)

{

  CActor *pActor = SceneObjects::GetActor(actorId);

  if (pActor!=NULL) // if actor is valid

  {

    CVector3 vLocation = pActor->GetModel()->vLocation;

    CCubeMap* pEnvMap = new CCubeMap();

    if (!pEnvMap->Init(256, PIh, D3DFMT_A8R8G8B8))

    {

      pEnvMap->CubeMap=NULL; // cleanup in case of error

      SAFE_DELETE(pEnvMap);

      return;

    }

    // the cube map class will create the environment map for us 

    pEnvMap->MapItensity = pActor->EnvIntensity;

    pEnvMap->Reflection(vLocation, actorId);

    // save in lookup table

    envMaps[actorId] = pEnvMap; 

  }

}



In the code above the cube map class will help us create the actual cube map by looping through the 6 surfaces and store the rendering result in a surface. Since this step requires us to clear the device and invoke the Begin-End scene calls, a good sport for all this is in the post rendering. If an actor with an environment map is moved, we will in theory have to re-generate the map. Because this step will eat performance you may not want to do this each frame, well it all depends on the situation.

Settings manager and serialization
The scene objects do not jump into the kernel by themselves, we need to specify what to load into the scene. There are many ways to do this. These days people love XML, so this may be a good thing to do, it is up to you. But I've taken a more old school approach, maybe because I hate XML. I once had a job where a guy made some totally complex insane code with XML and schemes. That's cool, but when the guy quite his job I was the one to fixed all the bugs. Eventually I ended up erasing all his work, made a new simple ASCII format and got the job done in a few days. YES, this is a totally stupid argument for not using XML, but it is one of the reasons why I sometimes choose to do something simpler. The gold here is to load actors (models) and light sources (and maybe more), and this is a simple job. This is what the data we want to load looks like:

// scene actors --------------------

{actor}=Models\test.x |

{actorpos}=0.0, 0.0, 0.0 |
{actorrot}=0, 0, 0 |
{actorscale}=1.0 |
{actorshader}=shader\diffuse_shadowcube.fxo |
{actordifftile}=1.0 ,1.0 |
{actorbumptile}=1.0 ,1.0 |

   :
{actor}+=Models\brik1.x |
{actorpos}+=0.0, -0.5, 0.0 |

   :

// light sources -------------------- 
{lighttype}=ambi |
{lightpos}=0.09, 20.58, 0 |
{lightdir}=0, 0, 1 |
{lightrange}=5 |
{lightitensity}=0.05 |
{lightcolor}=255, 255, 255 |

   :



Note the += sign when adding objects. A settings manager will do the loading with the help from a few templates. Since you properly want to do your loading in some other way, I will skip more comments about it here, and just show the way it is used. It is all about filling vectors with data, here goes:

 :

// fire up the settings manager for this scene

CSettingsManager set( scene );

if (!set.valid()) return false

 

// load in lists of objects

actor.clear(); set.setStringList("{actor}", actor);

actorpos.clear(); set.setVector3List("{actorpos}", actorpos);

actorrot.clear(); set.setVector3List("{actorrot}", actorrot);

actorscale.clear(); set.setList("{actorscale}", actorscale);

 :


Singletons
We have a few singleton objects: the kernel, camera, frustrum and DirectX Device etc. You may have a few more in your own projects (logging or sound-system), so lets make a template to helps us in this spot:

template<typename T>

class TSingleton

{

  static T* pSingleton;

public:

  static inline void Create()

  {

    if(pSingleton) return;

    new T();

  }

  static inline T& GetSingleton() { return *pSingleton; }

  static inline T* GetSingletonPtr() { return pSingleton; }

  static inline void Destroy()  

  {

    if(pSingleton)

      delete pSingleton;

    pSingleton=0;

  }

 

protected:

  TSingleton()

  {

    if (!pSingleton)

    {

       pSingleton = static_cast<T*>(this);

    } 

    else

    {

      // arrhh .. allready created!

    }

  }

  ~TSingleton()

  {

    if (pSingleton)

      pSingleton=0;

  }

};

 

//

// lets make a define to help us typing less

// This should go into a global .h file somewhere

#define DEVICE CRenderDevice::GetSingleton()

 


This singleton template is a well known trick, I'm sure you will find it usefull.

Screen distorts effects
In order to make screen distorts effects we need to render our back buffer into a texture and display that texture on a full screen quad. When this is up and running we can tweak the screen quad in the shader to anything crazy. I've made a small shock-wave effect in one of the samples, but the same principle apply for things like heat haze, blurring and so on. Here is the code where all this happen:

:

if (CScreenDistort::Active)

{

  // a) store backbuffer as the fist step

  pBackBuffer->BeginScene();

    CKernel::GetSingleton().Render();

    TXT.Render();

  pBackBuffer->EndScene();

  

  // b) Invoke distort effect on a screen quad with the current backbuffer

  CRenderDevice::GetSingleton().GetDevice()->Clear(0, NULL, ...);

  CRenderDevice::GetSingleton().GetDevice()->BeginScene();

 

   CScreenDistort::Update(pBackBuffer); // anything crazy!

 

  CRenderDevice::GetSingleton().GetDevice()->EndScene();

  CRenderDevice::GetSingleton().GetDevice()->Present(NULL, NULL, NULL, NULL);

}

:

If we don't have any distort effects, this step is just skipped and the kernel does the rendering as usual.

Vector and math
Well, we are doing 3D drawing, so it should come to no surprise that we need a 3D vector implementation. We also need classes for 2D vectors, math stuff, matrix, polygon-math and colors and properly a lot more. There is no magic behind those simple classes, so I think it is best that you just look at the code. As always, you may change it or make something much more efficient. Here is a few interesting methods:

// Lerp: linear interpolation

CVector3 CVector3::Lerp(const CVector3 &v2, float factor) const

{

  return CVector3( x*(1.0f-factor) + v2.x*factor,

                     y*(1.0f-factor) + v2.y*factor,

                       z*(1.0f-factor) + v2.z*factor);

}

 

// RotX: rotation around the x axis

CMatrix16 CMatrix16::RotX(const float angle)

{

  float cosA = cosf(angle), sinA = sinf(angle);

  return CMatrix16(1, 0, 0, 0,

                   0, cosA, -sinA, 0,

                   0, sinA, cosA, 0,

                   0, 0, 0, 1);

}

 

// Angle: return angle between to vectors

float CPolyMath::Angle(CVector3 vV1, CVector3 vV2)

{

  float n = (vV1.Magnitude())*(vV2.Magnitude());

  float a = acosf( vV1.Dot(vV2) / n );

  // make sure angle is valid

  if(_isnan(a)) return 0.0f;

  return a;

}



Logging
You will surly run into problems when loading and rendering a 3D scene. For this reason we need some kind of logging system, to help us track bugs, and ultimately auto create documents with performance bottlenecks, loading time etc. The log implementation I've added is so simple that it is almost not worth to talk about, and I strongly suggest that you implement a more complete system. However, with the current logging system you can dump text to a file, display a message box or write things in the title of the application.

// make it easy to use anywhere: LOG(.., "my int: %d", ival)

#define LOG CLog::get().Log

 

// log implementation

void CLog::Log(LOGTYPE lt, const char *msg, ...)

{

  txt[0] = '\0';

  va_list args;

  va_start(args, msg);

  vsprintf(txt, msg, args);

 

  switch (lt)

  {

    case WRITE:

    if (writeAccess)

    {

      strcat(txt, "\n");

      fprintf(pFile, txt);

    }

    break;

   case ERRORBOX:

     ShowCursor(true);

     MessageBox(NULL, txt, "", MB_OK);

     break;

   case TITLE:

    SetWindowText(mHwnd, txt);

    break;

   default: { break; }

  };

}


This leads us to the end of the basic framework. There are more classes in the source, but it's my feeling that it will be clear to you what is going on by just looking at the code. Once again, this framework is not necessary the correct way of doing things, it all depends on what kind of 3D project your are working on.


HLSL Shaders
Tokyo Drift: "when it comes to drifting, there is no wax-on wax-off .. you learn by doing it!". Same goes for shaders. It is fun to creating shaders and try different techniques. To get a thorough understanding of how shaders work I strongly suggest you read through the documentation in the DirectX SDK, as usual it explains everything you need to know, and this is not a good place to repeat what you can easily find elsewhere.

I'm just going to present a few useful things about shaders and how to integrate them into a framework. First, I believe the best you can do in you own 3D projects is to create a separate shader project in the solution, and add all you shaders to that project. Something like this:





If you right-click each .fx file you can set a custom build step, which will be:

fxc.exe /Gpp /Tfx_2_0 /Fo $(InputName).fxo $(InputPath)



This is for shader model 2.0, supported by all hardware today. You may use a much newer shader model. The shader files are stored as .fx files and compiled into .fxo. Even though the DirectX create effect method (D3DXCreateEffectFromFile) can load .fx files directly, you gain some performance by using .fxo when rendering. Different shaders all need some global variables, things like the world-view-proj matrix, light position, textures and so on. Good practice is to collect all those variables in one file and include that on in all shaders. Here goes a snapshot of this header file:

// Matrix

float4x4 matWorldViewProjection : WorldViewProjection;

float4x4 matWorld : World;

float4x4 matWorldView : WorldView;

float4x4 matWorldInverse : WorldInverse; 

  :

// Light Variables

float3 LightPos; 

float3 LightDir; 

float3 LightColor = float3(1,1,1); 

  :

// Textures

texture2D txBase; 

sampler2D sBase = sampler_state

{

  Texture = <txBase>;

  MinFilter = Linear;

  MagFilter = Linear;

  MipFilter = Linear;

  MaxAnisotropy = (maxAnisotropy);

};

 

texture3D txVolume; 

sampler3D sVolume = sampler_state

{

  Texture = <txVolume>;

  MinFilter = Linear;

  MagFilter = Linear;

  MipFilter = Linear;

  MaxAnisotropy = (maxAnisotropy);

};

:



Methods used by many shaders can also go into this include file:

// Bump map, with light tangent from TBN matrix 

float CalculateNL(in float4 bump, in float3 lightTangent)

{

  float3 L = normalize(lightTangent);

  float3 N = 2.0f*bump.rgb - 1.0f;

  return saturate(dot(N,L));

}

// Blurring

const float2 poisson6[6] = {float2(-0.326212f, -0.40581f), ..};

float4 BlurX6(sampler2D tSource, float2 texCoord, float discRadius)

{

  float4 cOut = tex2D(tSource, texCoord);

  cOut += tex2D(tSource, texCoord + poisson6[0]*discRadius);

  cOut += tex2D(tSource, texCoord + poisson6[1]*discRadius);

  cOut += tex2D(tSource, texCoord + poisson6[2]*discRadius);

  cOut += tex2D(tSource, texCoord + poisson6[3]*discRadius);

  cOut += tex2D(tSource, texCoord + poisson6[4]*discRadius);

  cOut += tex2D(tSource, texCoord + poisson6[5]*discRadius);

  return ( cOut/7.0f);

}

:



Intellisense do not work well with .fx files, so I recommend that you keep an easy-to-follow naming standard for everything, and follow that standard always. Shaders can grow quickly, especially if you create different implementation for each shader model.


ODE physics
Before you try to understand how the physics works around here, it is highly recommended that you download the ODE resource and read the documentation about it. This may seems like a long way to go, and it is. It is not a single hour job to get ODE up and running smoothly. But, it's defiantly much quicker using ODE compared to working out you own physic simulator, so give it a try.

Using external components such as physics (or sound) requires us to create some sort of facade implementation in order to access what we need. I've added a physic project to the solution which contains a set of static facade classes.



Physic objects are stored as actors with the physic parameter set to something different than 'none'. Since physic objects sort of belong to the Game project, I've added a physic task to the Game project, which will update the simulation each frame. First, what we need is a physic manager and we need it to load our geometry (actors) into the ODE world. In order to do this I've created a world data struct (tWorldData), here goes:

bool CPManager::LoadWorldGeometry()

{

  // Here we fill the WorldData list with the geometry from each actor 

  WorldData.clear(); 

 

  D3DXMATRIX matTrans, matRot;

  int count = 0;

  for ( vector<CActor*>::iterator itA=SceneObjects::actors.begin();  

                                        itA!=SceneObjects::actors.end(); itA++)

  {

     tWorldData wd;

     memset(&wd,0,sizeof(tWorldData));

     wd.name = (*itA)->Name;

     wd.actorId = (*itA)->UniqueID;

      :

     // get the vertex data from DirectX vertex buffer

     CXMesh *pMesh = (*itA)->GetModel()->GetMesh();

     LPD3DXMESH mesh = (static_cast<CXMesh*>(pMesh))->GetXMesh();

     BYTE *ptr=NULL;

     if( mesh->LockVertexBuffer(0, (void**) &ptr)==D3D_OK )

     {

       DWORD numVerts = mesh->GetNumVertices();

       DWORD stride = mesh->GetNumBytesPerVertex();

 

       wd.vertexcount = numVerts;

       wd.vertices = new dVector3[numVerts];

       for (DWORD i=0; i<numVerts; i++)

       {

         D3DXVECTOR3 *vPtr=(D3DXVECTOR3 *) ptr;

         CVector3 p = CVector3(vPtr->x, vPtr->y, vPtr->z);

         CMatrix::Transform(p, matRotTrans);

         wd.vertices[i][0] = p.x; 

         wd.vertices[i][1] = p.y; 

         wd.vertices[i][2] = p.z; 

         ptr += stride;

       }

       mesh->UnlockVertexBuffer();

     }

     else

       return false; // its a bad egg

 

    // get the index data from DirectX index buffer 

    LPDIRECT3DINDEXBUFFER9 pIB;

    mesh->GetIndexBuffer( &pIB );

    WORD* pIndices;

    if (pIB->Lock( 0, 0, (void**)&pIndices, 0 )==D3D_OK)

    {

      DWORD dwNumFaces = mesh->GetNumFaces();

      int j=0;

      wd.indexcount = (dwNumFaces*3);

      wd.indices = new int[wd.indexcount];

      for( DWORD i=0; i<dwNumFaces; i++ )

      {

        wd.indices[j] = pIndices[3*i+0]; j++;

        wd.indices[j] = pIndices[3*i+1]; j++;

        wd.indices[j] = pIndices[3*i+2]; j++;

      }

      pIB->Unlock();

      SAFE_RELEASE(pIB);

    }

    else

      return false; // its a bad egg number 2

 

    // now we can add this world data element into the list

    WorldData.push_back(wd);

  }

 

  return true;

}



Now we have a WorldData list, and it is time to put it into ODE:

void CPManager::CreateWorld(const char *scene)

{

  if (Contactgroup!=NULL) dJointGroupDestroy(Contactgroup);

  if (Space!=NULL) dSpaceDestroy(Space);

  if (World!=NULL) dWorldDestroy(World);

  

  // create a new world

  World = dWorldCreate();

  Space = dHashSpaceCreate(0);

  Contactgroup = dJointGroupCreate(0);

  // set gravity and other stuff (from the doc)

  dWorldSetGravity(World, 0, -1.0f, 0);

  dWorldSetCFM (World,1e-5);

  dWorldSetAutoDisableFlag (World,1);

  dWorldSetContactMaxCorrectingVel(World,0.5);

  dWorldSetContactSurfaceLayer(World,0.0001);

 

  // create trimesh for our world of actors

  for (int i=0; i<WorldData.size(); i++)

  {

    dTriMeshDataID data = dGeomTriMeshDataCreate();

    dGeomTriMeshDataBuildSimple(data, 

                                            (dReal*)WorldData[i].vertices,   

                                             WorldData[i].vertexcount,  

                                             WorldData[i].indices, 

                                             WorldData[i].indexcount);

    // build the trimesh geom for this actor in the WorldData

    worldMesh[i] = dCreateTriMesh(Space, data, 0, 0, 0);

    dGeomSetPosition(worldMesh[i], 0, 0, 0);

    dGeomSetData(worldMesh[i], (void*)PIDS::STATIC);

    dGeomSetBody(worldMesh[i], 0);

  }

  :

 

}



Notice the dGeomTriMeshDataBuildSimple call and the lines below it; this is the place where our geometry gets into ODE. What we need to do now is to create the physic objects we want to move around. That is, the actors which have the physic parameter set to box, sphere etc. Since all physic objects have common attributes, this is a good spot for some inheritance. Physic objects inherit from IPEntity and have create and render methods. Let's look a the create method first:

void CPBox::Create(dWorldID world, dSpaceID space,

                           dReal posx, dReal posy, dReal posz,

                           dReal rotx, dReal roty, dReal rotz, dReal angle)

{

  // the mesh is loaded in the constructor, make sure we have it

  if (pMesh==NULL) return;

  dim0 = CMath::Absf(pMesh->vBBoxMax.x - pMesh->vBBoxMin.x)+0.002f;

  dim1 = CMath::Absf(pMesh->vBBoxMax.y - pMesh->vBBoxMin.y)+0.002f;

  dim2 = CMath::Absf(pMesh->vBBoxMax.z - pMesh->vBBoxMin.z)+0.002f;

  Radius = pMesh->Radius;

 

  // according to ODE we can have collision problems when length is > 10

  // print warning if this is the case!

  float maxDim = max(max(dim0,dim1),dim2);

  if ( maxDim >= 10.0f )

  {

    LOG(WRITE, "Physic Box object is too large, maxDim: %f", maxDim);

  }

 

  // create a new (box) body and rotate it

  body = dBodyCreate(world);

  dBodySetPosition(body, posx, posy, posz);

  dMatrix3 R;

  dRFromAxisAndAngle(R, rotx, roty, rotz, angle);

  dBodySetRotation (body, R);

 

  // set the mass for the box, according to the dimensions

  // and create the box in ODE  

  dMassSetBox(&mass, Density, dim0, dim1, dim2);

  geom = dCreateBox(space, dim0, dim1, dim2);

  dGeomSetBody(geom, body);

  dBodySetMass(body, &mass);

  // set data for collision

  dBodySetData (body, (void*)PIDS::BOX);

  dGeomSetData (geom, (void*)PIDS::BOX);

  SetTextureMapList();

 

  // just place the prevous positiona bit away for the actual position

  vCenterOld.x = (float)posx + 1.0f;

  vCenterOld.y = (float)posy + 1.0f;

  vCenterOld.z = (float)posz + 1.0f;

  lightMapTrigger = MAX_FLOAT;

  occlusionTrigger = MAX_FLOAT;

  IsVisible=true;

  SetLocation(geom);

  queryLightList();

  // the box is enabled by default, so it can move around

  Enable();  

}



The create method for sphere and cylinder is almost the same, its all about knowing the dimensions of the object and call appropriate ODE functions. The drawing of a physic object it is just a matter of drawing the mesh loaded in the constructor and set the correct translation and rotation according to what ODE dictates. We also want to light up the physic object as it falls into the range of some omni or spot lights. In order to do this, we need to maintain a list of lights which touches the object as it is moved around and apply that list to the shader when rendering.

To get all this up and running, ODE needs to do a simulation each frame. This takes place in the manager, and from the ODE documentations we have to do the following copy-paste code:

void CPManager::SimLoop()

{

  double simstep = 0.0025; // simulation steps

  double dt = dsElapsedTime();

  int nrofsteps = (int) ceilf(dt/simstep);

 

  for (int i=0; i<nrofsteps; i++)

  {

    dSpaceCollide (Space, 0,&nearCallback);

    dWorldQuickStep (World, simstep);

    dJointGroupEmpty (Contactgroup);

  }

}



When ODE detects that two objects are colliding, we end up in the nearCallBack method, here is a code snippet of this method (it is quite long):

static void nearCallback (void *data, dGeomID o1, dGeomID o2)

{

  dBodyID b1 = dGeomGetBody(o1);

  dBodyID b2 = dGeomGetBody(o2);

 

  // exit without doing anything if the two bodies are connected by a joint

  if (b1 && b2 && dAreConnectedExcluding (b1,b2,dJointTypeContact))

    return;

 

  // get the data for this collision

  void *d2 = dGeomGetData(o2);

  void *d1 = dGeomGetData(o1);

  if (d1==NULL && d2==NULL) return;

  

  // if one of the objects are a box not enabled, exit!

  if ( b1 && !dBodyIsEnabled(b1) && 

      ((int)d1==(int)PIDS::BOX) &&

      ((int)d2==(int)PIDS::STATIC)) return;

 

  if ( b2 && !dBodyIsEnabled(b2) && 

      ((int)d2==(int)PIDS::BOX) &&

      ((int)d1==(int)PIDS::STATIC)) return;

   :

 

  // do we have any collisions ..

  if (int numc = dCollide(o1,o2,

                          MAX_CONTACTS,

                          &contact[0].geom, 

                          sizeof(dContact)))

  {

    for (int i=0; i<numc; i++)

    {

      dJointID c = dJointCreateContact(CPManager::World, ..);

      dJointAttach (c,b1,b2);

     

      // box hits box 

      if ( (int)d1==(int)PIDS::BOX && (int)d2==(int)PIDS::BOX )

      {

       // do stuff here .. 

      }

      :

      

      // box hits sphere 

      if ( (((int)d1==(int)PIDS::BOX) && ((int)d2==(int)PIDS::SPHERE)) ||

           (((int)d2==(int)PIDS::BOX) && ((int)d1==(int)PIDS::SPHERE)))

      {

        // do stuff here (play sound etc.)

      }

      :      

}



This callback is triggered by ODE, and we could now in theory say that we have integrated physics in our project. Well this is more or less true. If you look in the source code you will find that I've added quite a few more classes and functions. It may take some time to understand ODE, and sometimes the simulations just go crazy because of small parameter adjustments. Still, it is not that complex to use after all. The author of ODE: Russell Smith, has done a tremendous job! I leave it up to you to explore ODE further. Enjoy!


Top