Shape Outlining in TGE
by DavidRM · 05/29/2006 (10:58 am) · 39 comments
Download Code File
Summary
This resource draws outlines around shapes (DTS objects) based on the edges and faces of the object. Creating the necessary list of the edges and faces of the model is somewhat time-intensive, though, so this resource also includes a mechanism for saving and loading the edges and faces from a file.
Background
This code is based on the "Stencil Shadows in TGE" resource submitted by Brett Fattori back in 2004. None of the stencil shadow part of the resource has been retained, though. I was only interested in the outlining.
This resource also shares some changes in common with my own "Cel Shading in TGE" resource. However, this resource does not require you to have that resource installed.
Details
The method used to find the outline of a 3D object is based on a rather simple-sounding approach. In a nutshell:
Once you have the list of edges and faces for the object, this process is surprisingly snappy. Collecting the list of edges and faces, unfortunately, is nowhere near "snappy".
The edges and faces are found this way:
This process is lengthy because you can't assume that the vertices in a DTS object's vertex list are unique. This means you can't simply do vertex index checks to know if you've seen a particular edge before. Instead, you have to do rather expensive x-y-z comparisons on each vertex you've already visited. And the most straightforward way to do this is in a linear search through the edges you've already seen. It only sounds kludgy because it is. If anyone comes up with a way to optimize this part of the process, do please feel free to share.
Even though you only have to create these lists once, when you first load the object, the time required is still too long. For that reason, I implemented saving/loading of the edges and faces to a separate file (which can be shipped along with the game). The resulting file (.EDG) is also tied to the CRC of the DTS file, so if the DTS file changes, Torque knows to re-calculate it.
Finally, the process of creating and rendering the outline requires the objects transform. Getting this matrix into the TSMesh::render() function is the source of many of the changes.
As written, this resource will create and save the edge/face list of every DTS object. The first time you run your project with this resource, the initial mission load will be *much* slower (think 10-20 seconds). But after that, it should load and run as normal.
Disclaimer
Make a backup of your project files before making any of the following changes and certainly before overwriting with the files in the zip.
Files in the Resource Zip
The files in the resource zip are based on a clean TGE 1.4 code base, modified as described below. You'll need to put these files in their proper locations in the source folders:
There are also 2 new files:
These new files must be added to your project.
And now, the source code changes... (all changes are highlighted)
tsMesh.h Chanages
After:
Add:
In the TSMesh class declaration...
After:
Add:
Change:
To:
After:
Add:
Change:
To:
Now, in the TSSkinMesh class declaration...
Change:
To:
tsMesh.cc Changes
After:
Add:
Change:
To:
In TSMesh::render(), after:
Add:
Change:
To:
Change:
To:
To the end of the file add:
tsSortedMesh.h Changes
The changes in tsSortedMesh.h/.cc are required so that the virtual render() function is called properly. You may not want to have your sorted mesh objects (like the trees in the Stronghold mission) outlined. I don't...but I include it here for completeness.
In the TSSortedMesh class declaration change:
To:
tsSortedMesh.cc Changes
After:
Add:
Change:
To:
In TSSortedMesh::render(), after:
Add:
tsShapeInstance.h Changes
In the TSShapeInstance::ObjectInstance class declaration change:
To:
In the TSShapeInstance::MeshObjectInstance class declaration change:
To:
In the TSShapeInstance::DecalObjectInstance class declaration change:
To:
In the TSShapeInstance class declaration change:
To:
In the TSShapeInstance class declaration change:
To:
And change:
To:
TSShapeInstance.cc Changes
Change:
To:
In TSShapeInstance::TSShapeInstance() change:
To:
Change:
To:
In this second constructor TSShapeInstance::TSShapeInstance() change:
To:
Change:
To:
In TSShapeInstance::buildInstanceData() after:
Add:
In TSShapeInstance::MeshObjectInstance::renderVB() change:
To:
Change:
To:
In TSShapeInstance::render() change:
To:
Change:
To:
In (this) TSShapeInstance::render() change:
To:
Also in this TSShapeInstance::render() change:
To:
Change:
To:
Change:
To:
Change:
To:
In TSShapeInstance::MeshObjectInstance::render() after:
Add:
Change:
To:
Change:
To:
Change:
To:
tsPartInstance.cc Changes
In TSPartInstance::render() change:
To:
And change:
To:
shapeBase.cc Changes
In ShapeBase::onNewDataBlock() change:
To:
In ShapeBase::renderMountedImage() change:
To:
In ShapeBase::renderImage() change:
To:
shapeImage.cc Changes
In ShapeBase::setImage() change:
To:
player.cc Changes
In Player::renderMountedImage() change:
To:
And in Player::renderImage() change:
To:
Screen Shot

Wrapping Up
That does it for code changes. If you want to see the orc in the starter.fps outlined properly, you'll need to edit the example/starter.fps/server/scripts/player.cs file, under "datablock PlayerData(PlayerBody)" and set "emap = false;". If you don't, the environment mapping will cause the outline to render as sky-colored. While you're there add "computeCRC = true;" Might want to add "computeCRC = true;" to the crossbow.cs (under "ItemData(Crossbow)" and "ShapeBaseImageData(CrossbowImage)" ), as well.
The given changes will outline player models and weapons. Other objects, like statics, will have to have "computeCRC = true" added to their scripts, and have the object transform added to their render() calls.
If you come up with a way to get the object transform into TSMesh::render() without changing the function definition (for example using something like the static variables in tsShapeInstance.cc) you can simplify this considerably. If you can avoid changing that function header, you can avoid having to change tsSortedMesh and tsPartInstance.
Also, I didn't implement outlining for buildings/interiors or terrain. The existing outlining code usually works fine for those, or you can modify this resource to handle them. Supporting DTS objects was my goal.
If you have any questions, comments, refinements, optimizations, or whatever, feel free to contact me.
Have fun with it!
-David
Edit: Fixed a problem loading edge files.
Summary
This resource draws outlines around shapes (DTS objects) based on the edges and faces of the object. Creating the necessary list of the edges and faces of the model is somewhat time-intensive, though, so this resource also includes a mechanism for saving and loading the edges and faces from a file.
Background
This code is based on the "Stencil Shadows in TGE" resource submitted by Brett Fattori back in 2004. None of the stencil shadow part of the resource has been retained, though. I was only interested in the outlining.
This resource also shares some changes in common with my own "Cel Shading in TGE" resource. However, this resource does not require you to have that resource installed.
Details
The method used to find the outline of a 3D object is based on a rather simple-sounding approach. In a nutshell:
for each edge of a polygon
if one (but not both) faces adjacent to the edge is turned away from the camera
that is an outline edge
else if the edge has only a single face
that is an outline edgeOnce you have the list of edges and faces for the object, this process is surprisingly snappy. Collecting the list of edges and faces, unfortunately, is nowhere near "snappy".
The edges and faces are found this way:
for each polygon
for each edge of the polygon
if we haven't seen this edge before
add the edge, associated with the face
else
associate this face with the existing edgeThis process is lengthy because you can't assume that the vertices in a DTS object's vertex list are unique. This means you can't simply do vertex index checks to know if you've seen a particular edge before. Instead, you have to do rather expensive x-y-z comparisons on each vertex you've already visited. And the most straightforward way to do this is in a linear search through the edges you've already seen. It only sounds kludgy because it is. If anyone comes up with a way to optimize this part of the process, do please feel free to share.
Even though you only have to create these lists once, when you first load the object, the time required is still too long. For that reason, I implemented saving/loading of the edges and faces to a separate file (which can be shipped along with the game). The resulting file (.EDG) is also tied to the CRC of the DTS file, so if the DTS file changes, Torque knows to re-calculate it.
Finally, the process of creating and rendering the outline requires the objects transform. Getting this matrix into the TSMesh::render() function is the source of many of the changes.
As written, this resource will create and save the edge/face list of every DTS object. The first time you run your project with this resource, the initial mission load will be *much* slower (think 10-20 seconds). But after that, it should load and run as normal.
Disclaimer
Make a backup of your project files before making any of the following changes and certainly before overwriting with the files in the zip.
Files in the Resource Zip
The files in the resource zip are based on a clean TGE 1.4 code base, modified as described below. You'll need to put these files in their proper locations in the source folders:
ts\tsMesh.h ts\tsMesh.cc ts\tsPartInstance.cc ts\tsShapeInstance.h ts\tsShapeInstance.cc ts\tsSortedMesh.h ts\tsSortedMesh.cc game\player.cc game\shapeBase.cc game\shapeImage.cc
There are also 2 new files:
ts\custom\tsMeshOutline.h ts\custom\tsMeshOutline.cc
These new files must be added to your project.
And now, the source code changes... (all changes are highlighted)
tsMesh.h Chanages
After:
class TSMaterialList; class TSShapeInstance; struct RayInfo; class ConvexFeature;
Add:
class TSMaterialList; class TSShapeInstance; struct RayInfo; class ConvexFeature; [b][i]// shape outline resource class MeshEdgeList; // Mesh edge list class MeshOutline; // Mesh outline information // shape outline resource[/i][/b]
In the TSMesh class declaration...
After:
ToolVector<U8> encodedNorms;
ToolVector<U16> indices;
ToolVector<U16> mergeIndices; ///< the last so many verts merge with these
///< verts to form the next detail level
///< NOT IMPLEMENTED YETAdd:
ToolVector<U8> encodedNorms;
ToolVector<U16> indices;
ToolVector<U16> mergeIndices; ///< the last so many verts merge with these
///< verts to form the next detail level
///< NOT IMPLEMENTED YET
[b][i]// shape outline resource
MeshEdgeList *mEdgeList;
MeshOutline *mOutline;
// shape outline resource[/i][/b]Change:
virtual void render(S32 frame, S32 matFrame, TSMaterialList *);
To:
[b][i]// shape outline resource virtual void render(S32 frame, S32 matFrame, TSMaterialList *, const MatrixF *); // shape outline resource[/i][/b]
After:
/// methods used during assembly to share vertexand other info /// between meshes (and for skipping detail levels on load) S32 * getSharedData32(S32 parentMesh, S32 size, S32 ** source, bool skip); S8 * getSharedData8 (S32 parentMesh, S32 size, S8 ** source, bool skip);
Add:
/// methods used during assembly to share vertexand other info /// between meshes (and for skipping detail levels on load) S32 * getSharedData32(S32 parentMesh, S32 size, S32 ** source, bool skip); S8 * getSharedData8 (S32 parentMesh, S32 size, S8 ** source, bool skip); [b][i]// shape outline resource bool needEdgeList(); void buildEdgeList(); bool loadEdgeList(Stream &stream); bool saveEdgeList(Stream &stream); // shape outline resource[/i][/b]
Change:
TSMesh() : meshType(StandardMeshType) {
VECTOR_SET_ASSOCIATION(planeNormals);
VECTOR_SET_ASSOCIATION(planeConstants);
VECTOR_SET_ASSOCIATION(planeMaterials);
parentMesh = -1;
}To:
TSMesh() : meshType(StandardMeshType) {
VECTOR_SET_ASSOCIATION(planeNormals);
VECTOR_SET_ASSOCIATION(planeConstants);
VECTOR_SET_ASSOCIATION(planeMaterials);
parentMesh = -1;
[b][i]// shape outline resource
mEdgeList = NULL;
mOutline = NULL;
// shape outline resource[/i][/b]
}Now, in the TSSkinMesh class declaration...
Change:
// render methods.. void render(S32 frame, S32 matFrame, TSMaterialList *); void renderShadow(S32 frame, const MatrixF & mat, S32 dim, U32 * bits, TSMaterialList *);
To:
// render methods.. [b][i]// shape outline resource void render(S32 frame, S32 matFrame, TSMaterialList *, const MatrixF *); // shape outline resource[/i][/b] void renderShadow(S32 frame, const MatrixF & mat, S32 dim, U32 * bits, TSMaterialList *);
tsMesh.cc Changes
After:
#include "collision/convex.h" #include "core/frameAllocator.h" #include "platform/profiler.h"
Add:
#include "collision/convex.h" #include "core/frameAllocator.h" #include "platform/profiler.h" [b][i]// shape outline resource #include "sceneGraph/sceneGraph.h" #include "ts/custom/meshOutline.h" // shape outline resource[/i][/b]
Change:
void TSMesh::render(S32 frame, S32 matFrame, TSMaterialList * materials)
To:
[b][i]// shape outline resource void TSMesh::render(S32 frame, S32 matFrame, TSMaterialList * materials, const MatrixF *objectTransform) // shape outline resource[/i][/b]
In TSMesh::render(), after:
S32 drawType = getDrawType(draw.matIndex>>30);
glDrawElements(drawType,draw.numElements,GL_UNSIGNED_SHORT,&indices[draw.start]);
}Add:
S32 drawType = getDrawType(draw.matIndex>>30);
glDrawElements(drawType,draw.numElements,GL_UNSIGNED_SHORT,&indices[draw.start]);
}
[b][i]// shape outline resource
if (objectTransform)
{
Point3F cameraPos=gClientSceneGraph->getBaseCameraPosition();
if (!mOutline)
mOutline=new MeshOutline;
mOutline->updateOutline(this,frame,*objectTransform,cameraPos);
mOutline->drawOutline(this);
}
// shape outline resource[/i][/b]Change:
TSMesh::~TSMesh()
{
}To:
TSMesh::~TSMesh()
{
[b][i]// shape outline resource
if (mEdgeList)
delete mEdgeList;
if (mOutline)
delete mOutline;
// shape outline resource[/i][/b]
}Change:
void TSSkinMesh::render(S32 frame, S32 matFrame, TSMaterialList * materials)
{
// update verts and normals...
updateSkin();
// render...
Parent::render(frame,matFrame,materials);
}To:
[b][i]// shape outline resource
void TSSkinMesh::render(S32 frame, S32 matFrame, TSMaterialList * materials, const MatrixF *objectTransform)
// shape outline resource[/i][/b]
{
// update verts and normals...
updateSkin();
// render...
[b][i]// shape outline resource
Parent::render(frame,matFrame,materials,objectTransform);
// shape outline resource[/i][/b]
}To the end of the file add:
[b][i]// shape outline resource
bool TSMesh::needEdgeList()
{
U32 badMask = DecalMeshType|NullMeshType|Billboard|BillboardZAxis;
return (mEdgeList==NULL) && (indices.size()>0) && (!(meshType & badMask));
}
void TSMesh::buildEdgeList()
{
U32 badMask = DecalMeshType|NullMeshType|Billboard|BillboardZAxis;
if (!mEdgeList) {
// We only want edge lists on standard and skin meshes
if (indices.size() > 0)
{
if (!(meshType & badMask))
{
mEdgeList = new MeshEdgeList();
mEdgeList->BuildEdgeList(this);
}
}
}
// If the edge list doesn't contain edges, NULL the list
if (mEdgeList)
{
if (mEdgeList->mEdges.address() && mEdgeList->mEdges.size() == 0)
{
delete mEdgeList;
mEdgeList = NULL;
}
}
}
bool TSMesh::loadEdgeList(Stream &stream)
{
U32 badMask = DecalMeshType|NullMeshType|Billboard|BillboardZAxis;
if (!mEdgeList)
{
// We only want edge lists on standard and skin meshes
if (indices.size() > 0)
{
if (!(meshType & badMask))
{
mEdgeList = new MeshEdgeList();
return mEdgeList->LoadEdgeList(stream);
}
}
}
return true;
}
bool TSMesh::saveEdgeList(Stream &stream)
{
if (mEdgeList)
return mEdgeList->SaveEdgeList(stream);
return false;
}
// shape outline resource[/i][/b]tsSortedMesh.h Changes
The changes in tsSortedMesh.h/.cc are required so that the virtual render() function is called properly. You may not want to have your sorted mesh objects (like the trees in the Stronghold mission) outlined. I don't...but I include it here for completeness.
In the TSSortedMesh class declaration change:
void render(S32 frame, S32 matFrame, TSMaterialList *);
To:
[b][i]// shape outline resource void render(S32 frame, S32 matFrame, TSMaterialList *, const MatrixF *); // shape outline resource[/i][/b]
tsSortedMesh.cc Changes
After:
#include "ts/tsShapeInstance.h"
Add:
#include "ts/tsShapeInstance.h" [b][i]// shape outline resource #include "sceneGraph/sceneGraph.h" #include "ts/custom/meshOutline.h" // shape outline resource[/i][/b]
Change:
void TSSortedMesh::render(S32 frame, S32 matFrame, TSMaterialList * materials)
To:
[b][i]// shape outline resource void TSSortedMesh::render(S32 frame, S32 matFrame, TSMaterialList * materials, const MatrixF *objectTransform) // shape outline resource[/i][/b]
In TSSortedMesh::render(), after:
// determine next cluster...
if (cluster->frontCluster!=cluster->backCluster)
nextCluster = (mDot(cluster->normal,cameraCenter) > cluster->k) ? cluster->frontCluster : cluster->backCluster;
else
nextCluster = cluster->frontCluster;
} while (nextCluster>=0);Add:
// determine next cluster...
if (cluster->frontCluster!=cluster->backCluster)
nextCluster = (mDot(cluster->normal,cameraCenter) > cluster->k) ? cluster->frontCluster : cluster->backCluster;
else
nextCluster = cluster->frontCluster;
} while (nextCluster>=0);
[b][i]// shape outline resource
if (objectTransform)
{
Point3F cameraPos=gClientSceneGraph->getBaseCameraPosition();
if (!mOutline)
mOutline=new MeshOutline;
mOutline->updateOutline(this,frame,*objectTransform,cameraPos);
mOutline->drawOutline(this);
}
// shape outline resource[/i][/b]tsShapeInstance.h Changes
In the TSShapeInstance::ObjectInstance class declaration change:
/// Render! This draws the base-textured object.
virtual void render(S32 objectDetail, TSMaterialList *);To:
/// Render! This draws the base-textured object.
[b][i]// shape outline resource
virtual void render(S32 objectDetail, TSMaterialList *, const MatrixF *);
// shape outline resource[/i][/b]In the TSShapeInstance::MeshObjectInstance class declaration change:
void render(S32 objectDetail, TSMaterialList *);
To:
[b][i]// shape outline resource
void render(S32 objectDetail, TSMaterialList *, const MatrixF *);
// shape outline resource[/i][/b]In the TSShapeInstance::DecalObjectInstance class declaration change:
void render(S32 objectDetail, TSMaterialList *);
To:
[b][i]// shape outline resource
void render(S32 objectDetail, TSMaterialList *, const MatrixF *);
// shape outline resource[/i][/b]In the TSShapeInstance class declaration change:
virtual void render(const Point3F * objectScale=NULL); virtual void render(S32 dl, F32 intraDL = 0.0f, const Point3F * objectScale = NULL);
To:
[b][i]// shape outline resource 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); // shape outline resource[/i][/b]
In the TSShapeInstance class declaration change:
TSShapeInstance( const Resource<TSShape> & shape, bool loadMaterials = true); TSShapeInstance( TSShape * pShape, bool loadMaterials = true);
To:
[b][i]// shape outline resource TSShapeInstance( const Resource<TSShape> & shape, bool loadMaterials = true, U32 shapeCrc = 0); TSShapeInstance( TSShape * pShape, bool loadMaterials = true, U32 shapeCrc = 0); // shape outline resource[/i][/b]
And change:
void buildInstanceData(TSShape *, bool loadMaterials);
To:
[b][i]// shape outline resource void buildInstanceData(TSShape *, bool loadMaterials, U32 shapeCrc); // shape outline resource[/i][/b]
TSShapeInstance.cc Changes
Change:
TSShapeInstance::TSShapeInstance(const Resource<TSShape> & shape, bool loadMaterials)
To:
[b][i]// shape outline resource TSShapeInstance::TSShapeInstance(const Resource<TSShape> & shape, bool loadMaterials, U32 shapeCrc) // shape outline resource[/i][/b]
In TSShapeInstance::TSShapeInstance() change:
buildInstanceData(mShape, loadMaterials);
To:
[b][i]// shape outline resource buildInstanceData(mShape, loadMaterials, shapeCrc); // shape outline resource[/i][/b]
Change:
TSShapeInstance::TSShapeInstance(TSShape * _shape, bool loadMaterials)
To:
[b][i]// shape outline resource TSShapeInstance::TSShapeInstance(TSShape * _shape, bool loadMaterials, U32 shapeCrc) // shape outline resource[/i][/b]
In this second constructor TSShapeInstance::TSShapeInstance() change:
buildInstanceData(mShape, loadMaterials);
To:
[b][i]// shape outline resource buildInstanceData(mShape, loadMaterials, shapeCrc); // shape outline resource[/i][/b]
Change:
void TSShapeInstance::buildInstanceData(TSShape * _shape, bool loadMaterials)
To:
[b][i]// shape outline resource void TSShapeInstance::buildInstanceData(TSShape * _shape, bool loadMaterials, U32 shapeCRC) // shape outline resource[/i][/b]
In TSShapeInstance::buildInstanceData() after:
mGroundThread = NULL; mCurrentDetailLevel = 0;
Add:
mGroundThread = NULL;
mCurrentDetailLevel = 0;
[b][i]// shape outline resource
// see if edges have already been loaded
bool needMeshEdges=false;
for (dl=0; dl<mShape->details.size(); dl++)
{
// check meshes on this detail level...
S32 ss = mShape->details[dl].subShapeNum;
if (ss<0)
continue; // this is a billboard detail level
S32 start = mShape->subShapeFirstObject[ss];
S32 end = mShape->subShapeNumObjects[ss] + start;
S32 od = mShape->details[dl].objectDetailNum;
for (i=start; i<end; i++)
{
TSMesh * mesh = mMeshObjects[i].getMesh(od);
if (!mesh)
continue;
if (mesh->needEdgeList())
{
needMeshEdges=true;
break;
}
}
if (needMeshEdges)
break;
}
if (needMeshEdges && shapeCRC)
{
bool needBuildEdgeList=true;
// see if an edge file exists
char dtsFileName[512];
dSprintf(dtsFileName, sizeof(dtsFileName), "%s", mShape->mSourceResource->name);
char * dot = dStrstr((const char*)dtsFileName, ".dts");
if(dot)
*dot = '[[62876c9ebf547]]';
char edgeFileName[512];
dSprintf(edgeFileName, sizeof(edgeFileName), "%s/%s-%x.edg", mShape->mSourceResource->path, dtsFileName, shapeCRC);
if (ResourceManager->findFile(edgeFileName))
{
Stream * stream = 0;
stream = ResourceManager->openStream(edgeFileName);
if(stream)
{
needBuildEdgeList=false;
for (dl=0; dl<mShape->details.size(); dl++)
{
// check meshes on this detail level...
S32 ss = mShape->details[dl].subShapeNum;
S32 od = mShape->details[dl].objectDetailNum;
if (ss<0)
continue; // this is a billboard detail level
S32 start = mShape->subShapeFirstObject[ss];
S32 end = mShape->subShapeNumObjects[ss] + start;
for (i=start; i<end; i++)
{
TSMesh * mesh = mMeshObjects[i].getMesh(od);
if (!mesh)
continue;
if (!(mesh->loadEdgeList(*stream)))
{
needBuildEdgeList=true;
break;
}
}
if (needBuildEdgeList)
break;
}
ResourceManager->closeStream(stream);
}
}
if (needBuildEdgeList)
{
bool skipSave=false;
FileStream file;
if(!ResourceManager->openFileForWrite(file, edgeFileName))
skipSave=true;
for (dl=0; dl<mShape->details.size(); dl++)
{
// check meshes on this detail level...
S32 ss = mShape->details[dl].subShapeNum;
S32 od = mShape->details[dl].objectDetailNum;
if (ss<0)
continue; // this is a billboard detail level
S32 start = mShape->subShapeFirstObject[ss];
S32 end = mShape->subShapeNumObjects[ss] + start;
for (i=start; i<end; i++)
{
TSMesh * mesh = mMeshObjects[i].getMesh(od);
if (!mesh)
continue;
mesh->buildEdgeList();
if (!skipSave)
mesh->saveEdgeList(file);
}
}
if (!skipSave)
file.close();
}
}
// shape outline resource[/i][/b]In TSShapeInstance::MeshObjectInstance::renderVB() change:
if (m0->getMeshType() != TSMesh::StandardMeshType)
{
render(objectDetail, materials);
return;
}To:
if (m0->getMeshType() != TSMesh::StandardMeshType)
{
[b][i]// shape outline resource
render(objectDetail, materials, NULL);
// shape outline resource[/i][/b]
return;
}Change:
void TSShapeInstance::render(const Point3F * objectScale)
To:
[b][i]// shape outline resource void TSShapeInstance::render(const MatrixF *objectTransform, const Point3F * objectScale) // shape outline resource[/i][/b]
In TSShapeInstance::render() change:
...
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:
...
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)
To:
[b][i]// shape outline resource void TSShapeInstance::render(const MatrixF *objectTransform, S32 dl, F32 intraDL, const Point3F * objectScale) // shape outline resource[/i][/b]
In (this) TSShapeInstance::render() change:
...
for (i=start; i<end; i++)
mMeshObjects[i].render(od,mMaterialList);
...To:
...
for (i=start; i<end; i++)
[b][i]// shape outline resource
mMeshObjects[i].render(od,mMaterialList,objectTransform);
// shape outline resource[/i][/b]
...Also in this TSShapeInstance::render() change:
// render decals...
smRenderData.currentTransform = NULL;
for (i=start; i<end; i++)
mDecalObjects[i].render(od,mMaterialList);To:
// render decals...
smRenderData.currentTransform = NULL;
for (i=start; i<end; i++)
[b][i]// shape outline resource
mDecalObjects[i].render(od,mMaterialList,objectTransform);
// shape outline resource[/i][/b]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);
[b][i]// shape outline resource
render(NULL,mCurrentDetailLevel,mCurrentIntraDetailLevel);
// shape outline resource[/i][/b]
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);
[b][i]// shape outline resource
render(NULL,mCurrentDetailLevel,mCurrentIntraDetailLevel);
// shape outline resource[/i][/b]
glReadPixels(xcenter-(bmpWidth>>1),ycenter-(bmpHeight>>1),bmpWidth,bmpHeight,GL_RGB,GL_UNSIGNED_BYTE,(void*)whiteBmp->getBits(0));
...
}Change:
void TSShapeInstance::ObjectInstance::render(S32, TSMaterialList *)
To:
[b][i]// shape outline resource void TSShapeInstance::ObjectInstance::render(S32, TSMaterialList *, const MatrixF *) // shape outline resource[/i][/b]
Change:
void TSShapeInstance::MeshObjectInstance::render(S32 objectDetail, TSMaterialList * materials)
To:
[b][i]// shape outline resource void TSShapeInstance::MeshObjectInstance::render(S32 objectDetail, TSMaterialList * materials, const MatrixF *objectTransform) // shape outline resource[/i][/b]
In TSShapeInstance::MeshObjectInstance::render() after:
MatrixF * transform = getTransform();
Add:
MatrixF * transform = getTransform();
[b][i]// shape outline resource
MatrixF mat;
MatrixF *objectCurrrentTransform=NULL;
if (objectTransform)
{
mat=*objectTransform;
if (transform)
mat.mul(*transform);
objectCurrrentTransform=&mat;
}
// shape outline resource[/i][/b]Change:
if (TSShapeInstance::smRenderData.balloonShape)
{
glPushMatrix();
F32 & bv = TSShapeInstance::smRenderData.balloonValue;
glScalef(bv,bv,bv);
}
mesh->render(frame,matFrame,materials);
if (TSShapeInstance::smRenderData.balloonShape)
glPopMatrix();To:
if (TSShapeInstance::smRenderData.balloonShape)
{
glPushMatrix();
F32 & bv = TSShapeInstance::smRenderData.balloonValue;
glScalef(bv,bv,bv);
}
[b][i]// shape outline resource
mesh->render(frame,matFrame,materials,objectCurrrentTransform);
// shape outline resource[/i][/b]
if (TSShapeInstance::smRenderData.balloonShape)
glPopMatrix();Change:
mesh->setFade(visible);
mesh->render(frame,matFrame,materials);
mesh->clearFade();To:
mesh->setFade(visible);
[b][i]// shape outline resource
mesh->render(frame,matFrame,materials,objectCurrrentTransform);
// shape outline resource[/i][/b]
mesh->clearFade();Change:
void TSShapeInstance::DecalObjectInstance::render(S32 objectDetail, TSMaterialList * materials)
To:
[b][i]// shape outline resource void TSShapeInstance::DecalObjectInstance::render(S32 objectDetail, TSMaterialList * materials, const MatrixF *objectTransform) // shape outline resource[/i][/b]
tsPartInstance.cc Changes
In TSPartInstance::render() change:
for (i=0; i<mMeshObjects.size(); i++)
mMeshObjects[i]->render(od,mSourceShape->getMaterialList());To:
for (i=0; i<mMeshObjects.size(); i++)
[b][i]// shape outline resource
mMeshObjects[i]->render(od,mSourceShape->getMaterialList(),NULL);
// shape outline resource[/i][/b]And change:
for (i=0; i<mDecalObjects.size(); i++)
mDecalObjects[i]->render(od,mSourceShape->mMaterialList);To:
for (i=0; i<mDecalObjects.size(); i++)
[b][i]// shape outline resource
mDecalObjects[i]->render(od,mSourceShape->mMaterialList,NULL);
// shape outline resource[/i][/b]shapeBase.cc Changes
In ShapeBase::onNewDataBlock() change:
delete mShapeInstance;
mShapeInstance = new TSShapeInstance(mDataBlock->shape, isClientObject());
if (isClientObject())
mShapeInstance->cloneMaterialList();To:
delete mShapeInstance;
[b][i]// shape outline resource
mShapeInstance = new TSShapeInstance(mDataBlock->shape, isClientObject(), mDataBlock->mCRC);
// shape outline resource[/i][/b]
if (isClientObject())
mShapeInstance->cloneMaterialList();In ShapeBase::renderMountedImage() change:
image.shapeInstance->animate();
image.shapeInstance->render();To:
image.shapeInstance->animate();
[b][i]// shape outline resource
// local variable mat (MatrixF) holds the correct transform
image.shapeInstance->render(&mat);
// shape outline resource[/i][/b]In ShapeBase::renderImage() change:
mShapeInstance->animate();
mShapeInstance->render();To:
mShapeInstance->animate();
[b][i]// shape outline resource
MatrixF mat=getRenderTransform();
mShapeInstance->render(&mat);
// shape outline resource[/i][/b]shapeImage.cc Changes
In ShapeBase::setImage() change:
image.skinNameHandle = skinNameHandle;
image.shapeInstance = new TSShapeInstance(image.dataBlock->shape, isClientObject());
if (isClientObject()) {To:
image.skinNameHandle = skinNameHandle;
[b][i]// shape outline resource
image.shapeInstance = new TSShapeInstance(image.dataBlock->shape, isClientObject(), image.dataBlock->mCRC);
// shape outline resource[/i][/b]
if (isClientObject()) {player.cc Changes
In Player::renderMountedImage() change:
image.shapeInstance->animate();
image.shapeInstance->render();To:
image.shapeInstance->animate();
[b][i]// shape outline resource
// local variable mat (MatrixF) holds the correct transform
image.shapeInstance->render(&mat);
// shape outline resource[/i][/b]And in Player::renderImage() change:
mShapeInstance->animate();
mShapeInstance->render();To:
mShapeInstance->animate();
[b][i]// shape outline resource
MatrixF mat=getRenderTransform();
mShapeInstance->render(&mat);
// shape outline resource[/i][/b]Screen Shot

Wrapping Up
That does it for code changes. If you want to see the orc in the starter.fps outlined properly, you'll need to edit the example/starter.fps/server/scripts/player.cs file, under "datablock PlayerData(PlayerBody)" and set "emap = false;". If you don't, the environment mapping will cause the outline to render as sky-colored. While you're there add "computeCRC = true;" Might want to add "computeCRC = true;" to the crossbow.cs (under "ItemData(Crossbow)" and "ShapeBaseImageData(CrossbowImage)" ), as well.
The given changes will outline player models and weapons. Other objects, like statics, will have to have "computeCRC = true" added to their scripts, and have the object transform added to their render() calls.
If you come up with a way to get the object transform into TSMesh::render() without changing the function definition (for example using something like the static variables in tsShapeInstance.cc) you can simplify this considerably. If you can avoid changing that function header, you can avoid having to change tsSortedMesh and tsPartInstance.
Also, I didn't implement outlining for buildings/interiors or terrain. The existing outlining code usually works fine for those, or you can modify this resource to handle them. Supporting DTS objects was my goal.
If you have any questions, comments, refinements, optimizations, or whatever, feel free to contact me.
Have fun with it!
-David
Edit: Fixed a problem loading edge files.
About the author
#2
i tried this and it worked find only first time when run torqueDemo.
after that, torquedemo crashed down.
but if i delete all The resulting files (*.EDG) and run torqueDemo, it works again..
could you help me about this?
and one more thing.. how can i make the outline thinner?
thanks in advance.
05/05/2006 (1:42 am)
@davidi tried this and it worked find only first time when run torqueDemo.
after that, torquedemo crashed down.
but if i delete all The resulting files (*.EDG) and run torqueDemo, it works again..
could you help me about this?
and one more thing.. how can i make the outline thinner?
thanks in advance.
#3
Make sure that you have set "computeCRC = true" in the script files for the objects. That's the only thing I can think of that might cause the effect you're seeing.
Are you working from the 1.4 base? I don't mean the CVS "head", BTW. I'm referring to what's available from the 1.4 SDK installer. I don't know what might be different, but it's something to consider.
The line width is calculated based on the distance of the object from the camera in MeshOutline::updateOutline() in meshOutline.cc. This is the code:
You can easily adjust the maximum width and the distances.
-David
05/05/2006 (10:56 am)
Rookie,Make sure that you have set "computeCRC = true" in the script files for the objects. That's the only thing I can think of that might cause the effect you're seeing.
Are you working from the 1.4 base? I don't mean the CVS "head", BTW. I'm referring to what's available from the 1.4 SDK installer. I don't know what might be different, but it's something to consider.
The line width is calculated based on the distance of the object from the camera in MeshOutline::updateOutline() in meshOutline.cc. This is the code:
F32 distFromCamera=fabs((cameraPos-mesh->getCenter()).len());
if (distFromCamera<1.0)
mLineWidth=5;
else if (distFromCamera<5.0)
mLineWidth=4;
else if (distFromCamera<10.0)
mLineWidth=3;
else if (distFromCamera<20.0)
mLineWidth=2;
else
mLineWidth=1;You can easily adjust the maximum width and the distances.
-David
#4
i worked from the 1.4 base and did everything what you said.
it worked fine with release build but it crashed down with debug build.
with debug build, if i delete "weapon-xxxxxxxx.edg", it worked.
void TSShapeInstance::buildInstanceData(TSShape * _shape, bool loadMaterials, U32 shapeCRC)
{
....
S32 start = mShape->subShapeFirstObject[ss];
S32 end = mShape->subShapeNumObjects[ss] + start;
for (i=start; i
{
TSMesh * mesh = mMeshObjects[i].getMesh(od);
if (!mesh)
continue;
if (!(mesh->loadEdgeList(*stream))) ===>error here!
{
needBuildEdgeList=true;
break;
}
}
....
}
05/06/2006 (12:50 am)
David,i worked from the 1.4 base and did everything what you said.
it worked fine with release build but it crashed down with debug build.
with debug build, if i delete "weapon-xxxxxxxx.edg", it worked.
void TSShapeInstance::buildInstanceData(TSShape * _shape, bool loadMaterials, U32 shapeCRC)
{
....
S32 start = mShape->subShapeFirstObject[ss];
S32 end = mShape->subShapeNumObjects[ss] + start;
for (i=start; i
TSMesh * mesh = mMeshObjects[i].getMesh(od);
if (!mesh)
continue;
if (!(mesh->loadEdgeList(*stream))) ===>error here!
{
needBuildEdgeList=true;
break;
}
}
....
}
#5
If the load is giving the error, then the problem is likely in the save. Specifically, the file is smaller than expected. You could verify that by stepping through the loadEdgeList() function and seeing where the problem happens. You might even step through this section:
See if the EDG file is being created as expected.
After you do a run with no EDG file, look at the EDG file. How big is it?
Hope that helps.
-David
05/06/2006 (10:38 am)
Rookie,If the load is giving the error, then the problem is likely in the save. Specifically, the file is smaller than expected. You could verify that by stepping through the loadEdgeList() function and seeing where the problem happens. You might even step through this section:
if (needBuildEdgeList)
{
bool skipSave=false;
FileStream file;
if(!ResourceManager->openFileForWrite(file, edgeFileName))
skipSave=true;
for (dl=0; dl<mShape->details.size(); dl++)
{
...See if the EDG file is being created as expected.
After you do a run with no EDG file, look at the EDG file. How big is it?
Hope that helps.
-David
#6
However, I have a problem with my models... this works fine with the default orc, but with models created through the blender exporter, every edge of the shape is rendered as an outline edge. Do you have any ideas why this might be happening?
05/17/2006 (4:57 am)
I can confirm that loading .edg files is not working properly. Apparently (and I'm quite confused about this), during the loading process, tge will allocate an amount of memory equal to mCrc, which should be the crc of the shape... although this doesn't make any sense to me. Anyway, when the loading occurs, mCrc has not been initialized, and in visual studio it defaults to 0xcccccccc which is too much memory, so it crashes. Using other compilers or debugging environments it may work, nevertheless you want to make sure mCrc is initialized.However, I have a problem with my models... this works fine with the default orc, but with models created through the blender exporter, every edge of the shape is rendered as an outline edge. Do you have any ideas why this might be happening?
#7
I'm still digging to see if I can isolate what the difference is between that object and the others.
-David
05/17/2006 (1:58 pm)
As of this afternoon I have confirmed the .edg file load problem...but it's not on every object. I'm only getting the crash on the crossbow.dts. None of the other DTS objects that I mark for outlining have the problem.I'm still digging to see if I can isolate what the difference is between that object and the others.
-David
#8
-David
05/17/2006 (3:49 pm)
The edge file load problem is fixed now. I've updated the description and the zip file.-David
#9
05/29/2006 (11:18 am)
Holy shit this looks awsome
#10
05/29/2006 (12:11 pm)
This is the longest resource i've ever seen, looks great though
#11
-David
05/29/2006 (12:38 pm)
SphyxGames: Yah, I probably didn't need to describe *every* change...but once I got started, I figured I would keep going. Thanks.-David
#12
05/30/2006 (11:31 am)
Very nice David!
#13
Very well put together.
Do you know how this compares in per-frame performance to the method of:
each frame:
extrude each vertex of the mesh along its normal,
render only the backfaces.
that's the basic method used by these guys:

edit: added the actual url.
05/30/2006 (1:08 pm)
Nice David !Very well put together.
Do you know how this compares in per-frame performance to the method of:
each frame:
extrude each vertex of the mesh along its normal,
render only the backfaces.
that's the basic method used by these guys:

edit: added the actual url.
#14
Thanks. No, I don't know how it compares, performance-wise. I didn't try that particular approach.
Have they posted their code? I'd be interested in seeing how they did both the outlining and the cel shading. Inverting the outline based on the shading (or lack of it) adds a lot to the look (for their game; not sure it would look good in mine). I wonder if they had to use vertex/fragment shaders to get that effect. Might be able to get that effect with a different blend function. Hmm...
-David
05/30/2006 (2:00 pm)
Orion,Thanks. No, I don't know how it compares, performance-wise. I didn't try that particular approach.
Have they posted their code? I'd be interested in seeing how they did both the outlining and the cel shading. Inverting the outline based on the shading (or lack of it) adds a lot to the look (for their game; not sure it would look good in mine). I wonder if they had to use vertex/fragment shaders to get that effect. Might be able to get that effect with a different blend function. Hmm...
-David
#15
but they do outline their method here. (scroll down to Grant Clarke's post) - they're using shaders for the shading, and the inverted-shell approach for the outlining.
for our product we use the dynamic inverted-shell for outlining, and essentially a light-aligned cubemap for shading. No shaders needed, runs on a GeForce2 !
but there is a performance hit, so i'm always interested in alternatives.
cheers,
orion
05/31/2006 (1:51 pm)
Hey David - no, they haven't posted their code,but they do outline their method here. (scroll down to Grant Clarke's post) - they're using shaders for the shading, and the inverted-shell approach for the outlining.
for our product we use the dynamic inverted-shell for outlining, and essentially a light-aligned cubemap for shading. No shaders needed, runs on a GeForce2 !
but there is a performance hit, so i'm always interested in alternatives.
cheers,
orion
#16
void drawOutline(TSMest * mesh);
to
void drawOutline(TSMesh * mesh, GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
with all the changes that come with it - then theres many more possible uses like selecting objects, showing team designation, etc.
I know I like it much better than the shapebox or circle that most implimentations have :)
edit: hmmm, needs more work than that to make it truely useful in that respect - ah well, its a start...
06/02/2006 (1:04 am)
If you expose the RGBA variables in DrawOutline, say by changingvoid drawOutline(TSMest * mesh);
to
void drawOutline(TSMesh * mesh, GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
with all the changes that come with it - then theres many more possible uses like selecting objects, showing team designation, etc.
I know I like it much better than the shapebox or circle that most implimentations have :)
edit: hmmm, needs more work than that to make it truely useful in that respect - ah well, its a start...
#17
06/02/2006 (2:48 am)
Orion: Game Programming Gems 4 has an article on Real-Time Halftoning that looks similar.
#18
It looked fine in a static image, but when in motion it just looked like environment mapping of some sort instead of shading. I should revisit the GPG 4 article tho, thanks.
06/02/2006 (8:28 am)
We tried using some halftone-type patterns for the cubemap, but it didn't come out all that convincing.It looked fine in a static image, but when in motion it just looked like environment mapping of some sort instead of shading. I should revisit the GPG 4 article tho, thanks.
#19
wow your product, "PCDMusicLounge" looks great and fun!!
your characters look like "cel-shading" but you said you use "essentially a light-aligned cubemap for shading". what is it?
06/08/2006 (3:14 am)
@orionwow your product, "PCDMusicLounge" looks great and fun!!
your characters look like "cel-shading" but you said you use "essentially a light-aligned cubemap for shading". what is it?
#20
well "cel-shading" is just an appearance, and there's a variety of ways to get that appearance. the traditional approach is to use shaders, but for "the lounge", clint implemented a light-aligned cubemap. cubemaps are usually world-aligned and contain an image of the world, but this one is light-aligned and contains shading. i can't post the code, unfortunately, but that's the main idea.
06/08/2006 (8:23 am)
hey rookie, thanks!well "cel-shading" is just an appearance, and there's a variety of ways to get that appearance. the traditional approach is to use shaders, but for "the lounge", clint implemented a light-aligned cubemap. cubemaps are usually world-aligned and contain an image of the world, but this one is light-aligned and contains shading. i can't post the code, unfortunately, but that's the main idea.

Torque Owner kingkong