Cel Shading in TGE
by DavidRM · 05/29/2006 (10:57 am) · 35 comments
Download Code File
This method of cel shading based on multi-texturing and uses 2 passes. The resulting look, though, seems to be well worth it. And since you can limit which objects get the cel-shading (players, statics, etc) you don't have to take the 2-pass performance hit on everything. Also, I'm only interested in cel shading objects (DTS) not interiors or the terrain. Still, you could probably extrapolate how to cel shade those, as well, if you wanted.
NOTE: The changes described are based on a clean 1.4.
The files in the zip reflect all of these changes. Make sure you put them where they belong in the source tree:
ts\tsMesh.h
ts\tsMesh.cc
ts\tsShapeInstance.h
ts\tsShapeInstance.cc
game\player.cc
ALSO NOTE: If you use the files in the zip, backup your version of those files.
TSMesh.h Changes
Add these changes to TSMesh.h (new code highlighted:
TSMesh.cc Changes
Add these changes to TSMesh.cc (new code highlighted:
TSShapeInstance.h Changes
This one requires editing of existing code (not just adding new stuff).
Change:
To:
OK. Now to "just add new stuff". Code additions highlighted.
TSShapeInstance.cc Changes
Change:
To:
Change:
To:
Change:
To:
And now, for more "just add stuff":
OK. That's it for the core changes. Take a minute and make sure you can get a build. :)
Since the cel shading won't happen without the object transform, and since that parameter is optional (defaulting to NULL), you should be able to build and run and see everything shaded normally by OpenGL.
I went with this approach to make it easy to turn cel shading off and on for particular types of in-game objects. If you want cel shading...just pass in the object transform to the render() call.
For example, to turn on cel shading for the player, in player.cc change:
To:
And change:
To:
And that should do it. Re-build and you'll have cel shading on player models. Even vehicles (though you need to add the object transform to get it on the vehicle wheels).

That's it, I think. What I want to change/extend are:
* Work with interior lights (still waiting to get the lighting kit)
* Have a scriptable, per object cel shading texture
* Better integrate with Torque's multitexturing so I can get it all done in one pass
Known issues:
* Statics with translucent materials (like the trees in the starter.fps) show some artifacts as the cel shading is sometimes appears on top of the billboard thingy.
* Having to call updateSkin() in TSSkinMesh::renderCelShade() is a significant performance hit. You should look into a way to incorporate the cel shading in TSMesh::render() to take advantage of the bone transforms already done.
Comments and critique--and suggestions for improvements--are welcome.
Have fun with it.
-David
Credits
The cel shading articles by Sami "MENTAL" Hamlaoui at Nehe and GameDev were very useful. Specifically:
Lesson 37: Cel-Shading
Cel-Shading
Also, the resourced posted on GG by Alexander "Nanaki" Traverso:
Cel Shading with OpenGL
And I would like to thank Dave "Myopic Rhino" Astle for answering my newb OpenGL questions.
Edits
Edit: I had left out an #include in TSMesh.cc. And I had left a function header out of TSShapeInstance.h (in the zip file, not in the instructions above).
Edit: Needed to disable lighting in TSMesh::render().
Edit: If you have the Torque Lighting Kit installed, you will need to either remove the overexposure from DTS objects as described in this thread (viewable only by TLK licensees) or *don't* disable the lighting in TSMesh::render(). If you use the latter, depending on your ambient light settings, you might see some regular shading on your model, in addition to the cel shading. You'll have to decide which looks better to you and/or which best fits your project.
Edit: Fix for the "Player models sometimes show artifacts of the cel shading of other player models in the scene" problem.
Edit: Forgot the object transform in TSSkinMesh::renderCelShade().
This method of cel shading based on multi-texturing and uses 2 passes. The resulting look, though, seems to be well worth it. And since you can limit which objects get the cel-shading (players, statics, etc) you don't have to take the 2-pass performance hit on everything. Also, I'm only interested in cel shading objects (DTS) not interiors or the terrain. Still, you could probably extrapolate how to cel shade those, as well, if you wanted.
NOTE: The changes described are based on a clean 1.4.
The files in the zip reflect all of these changes. Make sure you put them where they belong in the source tree:
ts\tsMesh.h
ts\tsMesh.cc
ts\tsShapeInstance.h
ts\tsShapeInstance.cc
game\player.cc
ALSO NOTE: If you use the files in the zip, backup your version of those files.
TSMesh.h Changes
Add these changes to TSMesh.h (new code highlighted:
class TSMesh
{
protected:
...
void renderEnvironmentMap(S32 frame, S32 matFrame, TSMaterialList *);
void renderDetailMap(S32 frame, S32 matFrame, TSMaterialList *);
[b][i]virtual void renderCelShade(S32 frame, S32 matFrame, TSMaterialList *, const MatrixF *);[/i][/b]
...
static void initEnvironmentMapMaterials();
static void resetEnvironmentMapMaterials();
static void initDetailMapMaterials();
static void resetDetailMapMaterials();
[b][i]static void initCelShadeMaterials();
static void resetCelShadeMaterials();[/i][/b]
...
};
...
class TSSkinMesh : public TSMesh
{
public:
...
// render methods..
void render(S32 frame, S32 matFrame, TSMaterialList *);
void renderShadow(S32 frame, const MatrixF & mat, S32 dim, U32 * bits, TSMaterialList *);
[b][i]void renderCelShade(S32 frame, S32 matFrame, TSMaterialList *, const MatrixF *);[/i][/b]
...
};TSMesh.cc Changes
Add these changes to TSMesh.cc (new code highlighted:
...
#include "core/frameAllocator.h"
#include "platform/profiler.h"
[b][i]// for cel shading
#include "sceneGraph/sceneGraph.h"[/i][/b]
...
void TSMesh::render(S32 frame, S32 matFrame, TSMaterialList * materials)
{
...
// lock...
bool lockArrays = dglDoesSupportCompiledVertexArray();
if (lockArrays)
glLockArraysEXT(0,vertsPerFrame);
[b][i]// cel shading - begin
bool oldlighting=glIsEnabled(GL_LIGHTING);
glDisable(GL_LIGHTING);
// cel shading - end[/i][/b]
for (S32 i=0; i<primitives.size(); i++)
{
...
}
[b][i]// cel shading - begin
if (oldlighting) glEnable(GL_LIGHTING);
// cel shading - end[/i][/b]
// unlock...
if (lockArrays)
glUnlockArraysEXT();
...
}
...
void TSMesh::resetDetailMapMaterials()
{
...
}
[b][i]//-----------------------------------------------------
// TSMesh render cel shading (2 pass) methods
//-----------------------------------------------------
bool celShadeTextureBuilt=false;
GLuint celShadeTexture;
void buildCelShadeTexture()
{
if (!celShadeTextureBuilt)
{
// build cel shade texture
Vector<Point3F> celShades;
celShades.setSize(32);
for (S32 pp=0; pp<32; pp++)
{
F32 shade=1.0;
if (pp<2)
shade=0.75;
else if (pp<8)
shade=0.80;
celShades[pp]=Point3F(shade,shade,shade);
}
glGenTextures(1,&celShadeTexture);
glBindTexture(GL_TEXTURE_1D,celShadeTexture);
glTexParameteri(GL_TEXTURE_1D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexImage1D(GL_TEXTURE_1D,0,GL_RGB,32,0,GL_RGB,GL_FLOAT,celShades.address());
celShadeTextureBuilt=true;
}
}
void TSMesh::renderCelShade(S32 frame, S32 matFrame, TSMaterialList * materials, const MatrixF *objectTransform)
{
// most gl states assumed to be all set up...
// if we're here, then we're drawing cel shading in two passes...
if ((objectTransform==NULL) || (!dglDoesSupportARBMultitexture()))
return;
S32 firstVert = vertsPerFrame * frame;
const Point3F *normals = getNormals(firstVert);
LightManager * lightManager = gClientSceneGraph->getLightManager();
Point3F lightPos=this->getCenter();
objectTransform->mulP(lightPos);
if (lightManager->getSunLight())
lightPos-=lightManager->getSunLight()->mPos;
else
lightPos=-lightManager->getShadowLightDirection();
VectorF lightVector=lightPos;
MatrixF worldToObj=*objectTransform;
worldToObj.inverse();
worldToObj.mulV(lightVector);
lightVector.normalize();
// set up vertex arrays -- already enabled in TSShapeInstance::render
glVertexPointer(3,GL_FLOAT,0,&verts[firstVert]);
glNormalPointer(GL_FLOAT,0,normals);
// create vertex texture coordinates
static Vector<F32> cstverts;
cstverts.setSize(vertsPerFrame);
for (S32 vv=0; vv<vertsPerFrame; vv++)
{
Point3F tmpNormal=normals[vv];
// dot product of the vertex normal and light
F32 dotP = mDot(tmpNormal,lightVector);
if (dotP<0.0)
dotP=0.0;
cstverts[vv]=dotP;
}
glTexCoordPointer(1,GL_FLOAT,0,cstverts.address());
// lock...
bool lockArrays = dglDoesSupportCompiledVertexArray();
if (lockArrays)
glLockArraysEXT(0,vertsPerFrame);
for (S32 i=0; i<primitives.size(); i++)
{
TSDrawPrimitive & draw = primitives[i];
AssertFatal(draw.matIndex & TSDrawPrimitive::Indexed,"TSMesh::render: rendering of non-indexed meshes no longer supported");
if (!(draw.matIndex & TSDrawPrimitive::NoMaterial))
{
S32 flags = materials->getFlags(draw.matIndex & TSDrawPrimitive::MaterialMask);
// don't draw on translucent material - issues with env mapping
if (flags & TSMaterialList::Translucent)
continue;
}
glDrawElements(getDrawType(draw.matIndex>>30),draw.numElements,GL_UNSIGNED_SHORT,&indices[draw.start]);
}
// unlock...
if (lockArrays)
glUnlockArraysEXT();
}
void TSMesh::initCelShadeMaterials()
{
buildCelShadeTexture();
glEnable(GL_TEXTURE_1D);
glEnable(GL_BLEND);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CW);
glDepthMask(GL_TRUE);
glBlendFunc(GL_ZERO,GL_SRC_COLOR);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glBindTexture(GL_TEXTURE_1D,celShadeTexture);
glTexParameteri(GL_TEXTURE_1D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_1D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glDisable(GL_LIGHTING);
}
void TSMesh::resetCelShadeMaterials()
{
glEnable(GL_LIGHTING);
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_1D);
glDepthMask(GL_TRUE);
glDisable(GL_CULL_FACE);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}[/i][/b]
//-----------------------------------------------------
// TSMesh renderShadow
//-----------------------------------------------------
...
void TSSkinMesh::render(S32 frame, S32 matFrame, TSMaterialList * materials)
{
// update verts and normals...
updateSkin();
// render...
Parent::render(frame,matFrame,materials);
}
void TSSkinMesh::renderShadow(S32 frame, const MatrixF & mat, S32 dim, U32 * bits, TSMaterialList * materials)
{
// update verts and normals...
updateSkin();
// render...
Parent::renderShadow(frame,mat,dim,bits,materials);
}
[b][i]void TSSkinMesh::renderCelShade(S32 frame, S32 matFrame, TSMaterialList *materials, const MatrixF *objectTransform)
{
// update verts and normals...
updateSkin();
// render...
Parent::renderCelShade(frame,matFrame,materials,objectTransform);
}[/i][/b]TSShapeInstance.h Changes
This one requires editing of existing code (not just adding new stuff).
Change:
virtual void render(const Point3F * objectScale=NULL); virtual void render(S32 dl, F32 intraDL = 0.0f, const Point3F * objectScale = NULL);
To:
[b][i]virtual void render(const MatrixF *objectTransform=NULL, const Point3F * objectScale=NULL); virtual void render(const MatrixF *objectTransform, S32 dl, F32 intraDL = 0.0f, const Point3F * objectScale = NULL);[/i][/b]
OK. Now to "just add new stuff". Code additions highlighted.
class TSShapeInstance
{
public:
...
/// An ObjectInstance points to the renderable items in the shape...
struct ObjectInstance
{
...
/// Renders the environment map
virtual void renderEnvironmentMap(S32 objectDetail, TSMaterialList *);
/// Renders the detail map
virtual void renderDetailMap(S32 objectDetail, TSMaterialList *);
[b][i]/// Renders the cel shading
virtual void renderCelShade(S32 objectDetail, TSMaterialList *, const MatrixF *);[/i][/b]
/// Renders the fog texture
virtual void renderFog(S32 objectDetail, TSMaterialList*);
...
}
...
/// These are set up by default based on shape data
struct MeshObjectInstance : ObjectInstance
{
...
void renderEnvironmentMap(S32 objectDetail, TSMaterialList *);
void renderDetailMap(S32 objectDetail, TSMaterialList *);
[b][i]void renderCelShade(S32 objectDetail, TSMaterialList *, const MatrixF *);[/i][/b]
void renderShadow(S32 objectDetail, const MatrixF & mat, S32 dim, U32 * bits, TSMaterialList *);
...
}
...
void renderEnvironmentMap();
void renderDetailMap();
[b][i]void renderCelShade(const MatrixF *);[/i][/b]
void renderFog();
...
}TSShapeInstance.cc Changes
Change:
void TSShapeInstance::render(const Point3F * objectScale)
{
...
if (mCurrentIntraDetailLevel>alphaIn+alphaOut)
render(mCurrentDetailLevel,mCurrentIntraDetailLevel,objectScale);
else if (mCurrentIntraDetailLevel>alphaOut)
{
...
// first draw next detail level
if (mCurrentDetailLevel+1<mShape->details.size() && mShape->details[mCurrentDetailLevel+1].size>0.0f)
{
...
render(mCurrentDetailLevel+1,0.0f,objectScale);
}
...
render(mCurrentDetailLevel,mCurrentIntraDetailLevel,objectScale);
}
else
{
...
// first draw next detail level
if (mCurrentDetailLevel+1<mShape->details.size() && mShape->details[mCurrentDetailLevel+1].size>0.0f)
render(mCurrentDetailLevel+1,0.0f,objectScale);
...
render(mCurrentDetailLevel,mCurrentIntraDetailLevel,objectScale);
...
}
...
}To:
void TSShapeInstance::render([b][i]const MatrixF *objectTransform,[/i][/b] const Point3F * objectScale)
{
...
if (mCurrentIntraDetailLevel>alphaIn+alphaOut)
render([b][i]objectTransform,[/i][/b]mCurrentDetailLevel,mCurrentIntraDetailLevel,objectScale);
else if (mCurrentIntraDetailLevel>alphaOut)
{
...
// first draw next detail level
if (mCurrentDetailLevel+1<mShape->details.size() && mShape->details[mCurrentDetailLevel+1].size>0.0f)
{
...
render([b][i]objectTransform,[/i][/b]mCurrentDetailLevel+1,0.0f,objectScale);
}
...
render([b][i]objectTransform,[/i][/b]mCurrentDetailLevel,mCurrentIntraDetailLevel,objectScale);
}
else
{
...
// first draw next detail level
if (mCurrentDetailLevel+1<mShape->details.size() && mShape->details[mCurrentDetailLevel+1].size>0.0f)
render([b][i]objectTransform,[/i][/b]mCurrentDetailLevel+1,0.0f,objectScale);
...
render([b][i]objectTransform,[/i][/b]mCurrentDetailLevel,mCurrentIntraDetailLevel,objectScale);
...
}
...Change:
void TSShapeInstance::render(S32 dl, F32 intraDL, const Point3F * objectScale)
{
...
// render detail maps using a second pass?
if (twoPassDetailMap())
renderDetailMap();
// render environment map using second pass?
if (twoPassEnvironmentMap())
renderEnvironmentMap();
if (!supportBuffers || !renderDecalsX(ss,od))
{
...
}
// render fog if 2-passing it
if (twoPassFog())
renderFog();
...
}To:
void TSShapeInstance::render([b][i]const MatrixF *objectTransform,[/i][/b] S32 dl, F32 intraDL, const Point3F * objectScale)
{
...
// render detail maps using a second pass?
if (twoPassDetailMap())
renderDetailMap();
// render environment map using second pass?
if (twoPassEnvironmentMap())
renderEnvironmentMap();
if (!supportBuffers || !renderDecalsX(ss,od))
{
...
}
[b][i]// drm - render cel shade
if (objectTransform)
renderCelShade(objectTransform);[/i][/b]
// render fog if 2-passing it
if (twoPassFog())
renderFog();
...
}Change:
GBitmap * TSShapeInstance::snapshot(U32 width, U32 height, bool mip, MatrixF & cameraMatrix,bool hiQuality)
{
...
// take a snapshot of the shape with a black background...
glClearColor(0,0,0,0);
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
GBitmap * blackBmp = new GBitmap;
blackBmp->allocateBitmap(bmpWidth,bmpHeight,false,GBitmap::RGB);
render(mCurrentDetailLevel,mCurrentIntraDetailLevel);
glReadPixels(xcenter-(bmpWidth>>1),ycenter-(bmpHeight>>1),bmpWidth,bmpHeight,GL_RGB,GL_UNSIGNED_BYTE,(void*)blackBmp->getBits(0));
// take a snapshot of the shape with a white background...
glClearColor(1,1,1,1);
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
GBitmap * whiteBmp = new GBitmap;
whiteBmp->allocateBitmap(bmpWidth,bmpHeight,false,GBitmap::RGB);
render(mCurrentDetailLevel,mCurrentIntraDetailLevel);
glReadPixels(xcenter-(bmpWidth>>1),ycenter-(bmpHeight>>1),bmpWidth,bmpHeight,GL_RGB,GL_UNSIGNED_BYTE,(void*)whiteBmp->getBits(0));
...
}To:
GBitmap * TSShapeInstance::snapshot(U32 width, U32 height, bool mip, MatrixF & cameraMatrix,bool hiQuality)
{
...
// take a snapshot of the shape with a black background...
glClearColor(0,0,0,0);
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
GBitmap * blackBmp = new GBitmap;
blackBmp->allocateBitmap(bmpWidth,bmpHeight,false,GBitmap::RGB);
render([b][i]NULL,[/i][/b]mCurrentDetailLevel,mCurrentIntraDetailLevel);
glReadPixels(xcenter-(bmpWidth>>1),ycenter-(bmpHeight>>1),bmpWidth,bmpHeight,GL_RGB,GL_UNSIGNED_BYTE,(void*)blackBmp->getBits(0));
// take a snapshot of the shape with a white background...
glClearColor(1,1,1,1);
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
GBitmap * whiteBmp = new GBitmap;
whiteBmp->allocateBitmap(bmpWidth,bmpHeight,false,GBitmap::RGB);
render([b][i]NULL,[/i][/b]mCurrentDetailLevel,mCurrentIntraDetailLevel);
glReadPixels(xcenter-(bmpWidth>>1),ycenter-(bmpHeight>>1),bmpWidth,bmpHeight,GL_RGB,GL_UNSIGNED_BYTE,(void*)whiteBmp->getBits(0));
...
}And now, for more "just add stuff":
void TSShapeInstance::renderDetailMap()
{
...
}
[b][i]void TSShapeInstance::renderCelShade(const MatrixF *objectTransform)
{
S32 dl = smRenderData.detailLevel;
const TSDetail * detail = &mShape->details[dl];
S32 ss = detail->subShapeNum;
S32 od = detail->objectDetailNum;
S32 start = mShape->subShapeFirstObject[ss];
S32 end = mShape->subShapeNumObjects[ss] + start;
// set up gl environment for the cel shading
TSMesh::initCelShadeMaterials();
// run through objects and render cel shading
smRenderData.currentTransform = NULL;
for (S32 i=start; i<end; i++)
mMeshObjects[i].renderCelShade(od,mMaterialList,objectTransform);
// if we have a matrix pushed, pop it now
if (smRenderData.currentTransform)
glPopMatrix();
// restore gl state
TSMesh::resetCelShadeMaterials();
}[/i][/b]
S32 TSShapeInstance::getCurrentDetail()
{
return mCurrentDetailLevel;
}
...
void TSShapeInstance::ObjectInstance::renderDetailMap(S32, TSMaterialList *)
{
...
}
[b][i]void TSShapeInstance::ObjectInstance::renderCelShade(S32, TSMaterialList *, const MatrixF *)
{
AssertFatal(0,"TSShapeInstance::ObjectInstance::renderCelShade: no default render method.");
}[/i][/b]
void TSShapeInstance::ObjectInstance::renderFog(S32, TSMaterialList*)
{
...
}
...
void TSShapeInstance::MeshObjectInstance::renderDetailMap(S32 objectDetail, TSMaterialList * materials)
{
...
}
[b][i]void TSShapeInstance::MeshObjectInstance::renderCelShade(S32 objectDetail, TSMaterialList * materials, const MatrixF *objectTransform)
{
if (visible>0.01f)
{
TSMesh * mesh = getMesh(objectDetail);
if (mesh)
{
MatrixF * transform = getTransform();
if (transform != TSShapeInstance::smRenderData.currentTransform)
{
if (TSShapeInstance::smRenderData.currentTransform)
glPopMatrix();
if (transform)
{
glPushMatrix();
dglMultMatrix(transform);
}
TSShapeInstance::smRenderData.currentTransform = transform;
}
if (TSShapeInstance::smRenderData.balloonShape)
{
glPushMatrix();
F32 & bv = TSShapeInstance::smRenderData.balloonValue;
glScalef(bv,bv,bv);
}
if (objectTransform)
{
MatrixF mat=*objectTransform;
if (transform)
mat.mul(*transform);
mesh->renderCelShade(frame,matFrame,materials,&mat);
}
else
mesh->renderCelShade(frame,matFrame,materials,NULL);
if (TSShapeInstance::smRenderData.balloonShape)
glPopMatrix();
}
}
}[/i][/b]
void TSShapeInstance::MeshObjectInstance::renderShadow(S32 objectDetail, const MatrixF & mat, S32 dim, U32 * bits, TSMaterialList * materialList)
{
...
}OK. That's it for the core changes. Take a minute and make sure you can get a build. :)
Since the cel shading won't happen without the object transform, and since that parameter is optional (defaulting to NULL), you should be able to build and run and see everything shaded normally by OpenGL.
I went with this approach to make it easy to turn cel shading off and on for particular types of in-game objects. If you want cel shading...just pass in the object transform to the render() call.
For example, to turn on cel shading for the player, in player.cc change:
...
void Player::renderMountedImage(SceneState* state, ShapeImageRenderImage* rimage)
{
...
image.shapeInstance->setupFog(fogAmount,state->getFogColor());
image.shapeInstance->animate();
image.shapeInstance->render();
...
}
...To:
...
void Player::renderMountedImage(SceneState* state, ShapeImageRenderImage* rimage)
{
...
image.shapeInstance->setupFog(fogAmount,state->getFogColor());
image.shapeInstance->animate();
[b][i]// local variable mat (MatrixF) holds the correct transform
image.shapeInstance->render(&mat);[/i][/b]
...
}
...And change:
...
void Player::renderImage(SceneState* state, SceneRenderImage* image)
{
...
TSMesh::setOverrideFade( mFadeVal );
mShapeInstance->setupFog(fogAmount,state->getFogColor());
mShapeInstance->animate();
mShapeInstance->render();
TSMesh::setOverrideFade( 1.0 );
...
}To:
...
void Player::renderImage(SceneState* state, SceneRenderImage* image)
{
...
TSMesh::setOverrideFade( mFadeVal );
mShapeInstance->setupFog(fogAmount,state->getFogColor());
mShapeInstance->animate();
[b][i]MatrixF mat=getRenderTransform();
mShapeInstance->render(&mat);[/i][/b]
TSMesh::setOverrideFade( 1.0 );
...
}And that should do it. Re-build and you'll have cel shading on player models. Even vehicles (though you need to add the object transform to get it on the vehicle wheels).

That's it, I think. What I want to change/extend are:
* Work with interior lights (still waiting to get the lighting kit)
* Have a scriptable, per object cel shading texture
* Better integrate with Torque's multitexturing so I can get it all done in one pass
Known issues:
* Statics with translucent materials (like the trees in the starter.fps) show some artifacts as the cel shading is sometimes appears on top of the billboard thingy.
* Having to call updateSkin() in TSSkinMesh::renderCelShade() is a significant performance hit. You should look into a way to incorporate the cel shading in TSMesh::render() to take advantage of the bone transforms already done.
Comments and critique--and suggestions for improvements--are welcome.
Have fun with it.
-David
Credits
The cel shading articles by Sami "MENTAL" Hamlaoui at Nehe and GameDev were very useful. Specifically:
Lesson 37: Cel-Shading
Cel-Shading
Also, the resourced posted on GG by Alexander "Nanaki" Traverso:
Cel Shading with OpenGL
And I would like to thank Dave "Myopic Rhino" Astle for answering my newb OpenGL questions.
Edits
Edit: I had left out an #include in TSMesh.cc. And I had left a function header out of TSShapeInstance.h (in the zip file, not in the instructions above).
Edit: Needed to disable lighting in TSMesh::render().
Edit: If you have the Torque Lighting Kit installed, you will need to either remove the overexposure from DTS objects as described in this thread (viewable only by TLK licensees) or *don't* disable the lighting in TSMesh::render(). If you use the latter, depending on your ambient light settings, you might see some regular shading on your model, in addition to the cel shading. You'll have to decide which looks better to you and/or which best fits your project.
Edit: Fix for the "Player models sometimes show artifacts of the cel shading of other player models in the scene" problem.
Edit: Forgot the object transform in TSSkinMesh::renderCelShade().
About the author
#2
04/21/2006 (6:36 pm)
As I understand it, making a toggle switch would be as simple as adding any manner of boolean check and changing his calls to render be like.if(yourBoolThing) mShapeInstance->render(&mat); else mShapeInstance->render();
#3
I had to do some updates/oops-fixes on this. I hope the issues didn't annoy anyone too much.
-David
04/21/2006 (7:46 pm)
Ramen: I'm not sure what the performance hit is. It's not horrible. Eyeballing framerates with it on vs off, I haven't noticed anything significant.I had to do some updates/oops-fixes on this. I hope the issues didn't annoy anyone too much.
-David
#4
tsShapeInstance.cc on line 1621, the wrong line got commented out...
Other than that, YAY Cel Shading!
04/21/2006 (11:31 pm)
I found ONE error(AT least this was the case with the download)tsShapeInstance.cc on line 1621, the wrong line got commented out...
Other than that, YAY Cel Shading!
#6
05/02/2006 (7:49 am)
@david : great resource!! thanks.
#7
i'm a OpenGL newbie.
i found another resource for cel-shading at TDN.
==> tdn.garagegames.com/wiki/WorldBuilding/Visual_Style/Cel_Shading/Code
can anybody tell me the differeces between this one by david and the other at TDN?
thanks in advance..
05/05/2006 (5:24 am)
hey guys!i'm a OpenGL newbie.
i found another resource for cel-shading at TDN.
==> tdn.garagegames.com/wiki/WorldBuilding/Visual_Style/Cel_Shading/Code
can anybody tell me the differeces between this one by david and the other at TDN?
thanks in advance..
#8
That resource on the TDN is a method of outlining shapes, buildings, and terrains using OpenGL. Actual cel shading doesn't occur (though there is something in the terrain modifications about "toon colors"; I never saw that have much effect).
This resource doesn't do any outlining. It overrides the OpenGL lighting to shade shapes with the "color bands" of comic books or cel cartoon animation.
I also have a resource for outlining that can be combined with this resource to get both the cel shading and the black outlines. My resource provides more consistent and appropriate results, I think, than what's in the TDN right now.
-David
05/05/2006 (10:51 am)
Rookie,That resource on the TDN is a method of outlining shapes, buildings, and terrains using OpenGL. Actual cel shading doesn't occur (though there is something in the terrain modifications about "toon colors"; I never saw that have much effect).
This resource doesn't do any outlining. It overrides the OpenGL lighting to shade shapes with the "color bands" of comic books or cel cartoon animation.
I also have a resource for outlining that can be combined with this resource to get both the cel shading and the black outlines. My resource provides more consistent and appropriate results, I think, than what's in the TDN right now.
-David
#9
i tried that resource on the TDN.
i cannot see "toon colors" do any effect and outlines are kind of dirty i think.
i love your work much more.
thank you for your help!!!
-Rookie
05/05/2006 (7:56 pm)
David,i tried that resource on the TDN.
i cannot see "toon colors" do any effect and outlines are kind of dirty i think.
i love your work much more.
thank you for your help!!!
-Rookie
#10
-David
05/12/2006 (2:48 pm)
This week I discovered a bug with TSSkinMesh objects. The fix has been incorporated into this resource, and in the zip file.-David
#11
There is a small problem with the files you uploaded, i guess that in tsMesh.cc the function TSSkinMesh::renderCelShade should be something like this:
05/17/2006 (4:46 am)
Great resource David! This makes tge even more useful.There is a small problem with the files you uploaded, i guess that in tsMesh.cc the function TSSkinMesh::renderCelShade should be something like this:
// cel shading
void TSSkinMesh::renderCelShade(S32 frame, S32 matFrame, TSMaterialList *materials, const MatrixF* objectTransform)
{
// update verts and normals...
updateSkin();
// render...
Parent::renderCelShade(frame,matFrame,materials, objectTransform);
}
#12
-David
05/17/2006 (12:15 pm)
Hadoken: Good catch. Thanks. I've updated the resource and the zip file.-David
#13
05/30/2006 (6:56 am)
Hotness
#14
Awesome resource! I was able to apply the patch and compile it without any problems whatsoever. Well done! Thanks!
Thought people might be interested in seeing what Kork looks like with a cel-shaded facelift.

In case anyone's curious, you can grab the texture that I used to generate that picture right here.
Again, two thumbs up! Truly a wonderful community asset.
Also, I'm quite stoked to hear PBN's under weigh again. Excellent!
05/31/2006 (4:48 pm)
Hey RM!Awesome resource! I was able to apply the patch and compile it without any problems whatsoever. Well done! Thanks!
Thought people might be interested in seeing what Kork looks like with a cel-shaded facelift.
In case anyone's curious, you can grab the texture that I used to generate that picture right here.
Again, two thumbs up! Truly a wonderful community asset.
Also, I'm quite stoked to hear PBN's under weigh again. Excellent!
#15
Good job David!
~Chad
05/31/2006 (11:07 pm)
I already messed with cel shading in TGE and TSE and developed my own resources, however, this resource "TAKES THE CAKE"... because it builds clean and newbies who are just learning how to alter the engine can do it if they fallow the instructions. It works great! It's a simple solution.Good job David!
~Chad
#16
i'm using GuiObjectView.
and cel-shading isn't working properly..
could you tell me how i can modify GuiObjectView properly?
thank you in advance!!
06/08/2006 (5:05 am)
@davidi'm using GuiObjectView.
and cel-shading isn't working properly..
could you tell me how i can modify GuiObjectView properly?
thank you in advance!!
#17
-David
06/08/2006 (8:45 am)
@Rookie: The trick is getting the proper transform.void GuiObjectView::renderWorld( const RectI &updateRect )
{
...
// If this is a mounted object transform to the correct position
if (mMeshObjects.mMesh[i].parentNode != -1)
{
MatrixF mat;
getObjectTransform( &mat, i );
glPushMatrix();
dglMultMatrix( &mat );
//mMeshObjects.mMesh[i].mesh->render();
mMeshObjects.mMesh[i].mesh->render(&mat);
glPopMatrix();
}
else
{
//mMeshObjects.mMesh[i].mesh->render();
MatrixF mat(true);
mMeshObjects.mMesh[i].mesh->render(&mat);
}
...
}-David
#18
now GuiObjectView shows cel shading properly without lightDirection.
even though i change lightDirection, 2 tone shadow in "buildCelShadeTexture" doesn't change.
i guess i have to put mat properly related to lightDirection. right?
but that's all i can guess now as 3d beginner. could you tell me how?
always thank you for your kind answer.
06/08/2006 (10:42 am)
thanks, david!now GuiObjectView shows cel shading properly without lightDirection.
even though i change lightDirection, 2 tone shadow in "buildCelShadeTexture" doesn't change.
i guess i have to put mat properly related to lightDirection. right?
but that's all i can guess now as 3d beginner. could you tell me how?
always thank you for your kind answer.
#19
In TSMesh::renderCelShade() you might change:
to:
Which, I think, is what getShadowLightDirection() returns if it can't find anything else. But at least you'll be explicit about it.
-David
06/08/2006 (1:45 pm)
Unfortunately...I haven't fully worked out how to get the light direction from GuiObjectView.In TSMesh::renderCelShade() you might change:
if (lightManager->getSunLight())
lightPos-=lightManager->getSunLight()->mPos;
else
lightPos=-lightManager->getShadowLightDirection();to:
if (lightManager->getSunLight())
lightPos-=lightManager->getSunLight()->mPos;
else
lightPos.set(0.57735f, 0.57735f, 0.57735f);Which, I think, is what getShadowLightDirection() returns if it can't find anything else. But at least you'll be explicit about it.
-David
#20
06/09/2006 (6:06 pm)
Awesome!!! 
Torque Owner Cinder Games