dhewm3/neo/d3xp/physics/Clip.cpp
Daniel Gibson c22965bf58 Work around assertion in alphalabs4, fix #409
In the savegame from that bugreport, "monster_zsec_shotgun_12" was
lying dead pretty much at its spawn point.
script/map_alphalabs4.script moves those "ride_of_death" platforms
around, and at the end of a cycle teleports "ride_of_death*_parent" back
to its starting position - and the "ride_of_death*" bound to it, which
is a pushing mover, just gets dragged along by the physics code and thus
can collide with that zombie cadaver, which then tries to push it along,
causing that assertion in TestHugeTranslation().
This is a map bug - Doom3 even prints a warning:
 WARNING: script/map_alphalabs4.script(722): Thread
  'map_alphalabs4::RideOfDeathPath': teleported 'ride_of_death2_parent'
  which has the pushing mover 'ride_of_death2' bound to it
So I just disable that assertion for this specific case..
Also moved the assertion behind the corresponding warning, so that gets
printed before the assertion kills the game..

Also a small change to CMakeLists.txt that should make enabling
LINUX_RELEASE_BINS after CMake has already been run without it work
2022-05-16 05:43:47 +02:00

1674 lines
44 KiB
C++

/*
===========================================================================
Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.
This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").
Doom 3 Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Doom 3 Source Code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Doom 3 Source Code. If not, see <http://www.gnu.org/licenses/>.
In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code. If not, please request a copy in writing from id Software at the address below.
If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.
===========================================================================
*/
#include "sys/platform.h"
#include "gamesys/SaveGame.h"
#include "Entity.h"
#include "Game_local.h"
#include "physics/Clip.h"
#define MAX_SECTOR_DEPTH 12
#define MAX_SECTORS ((1<<(MAX_SECTOR_DEPTH+1))-1)
typedef struct clipSector_s {
int axis; // -1 = leaf node
float dist;
struct clipSector_s * children[2];
struct clipLink_s * clipLinks;
} clipSector_t;
typedef struct clipLink_s {
idClipModel * clipModel;
struct clipSector_s * sector;
struct clipLink_s * prevInSector;
struct clipLink_s * nextInSector;
struct clipLink_s * nextLink;
} clipLink_t;
typedef struct trmCache_s {
idTraceModel trm;
int refCount;
float volume;
idVec3 centerOfMass;
idMat3 inertiaTensor;
} trmCache_t;
idVec3 vec3_boxEpsilon( CM_BOX_EPSILON, CM_BOX_EPSILON, CM_BOX_EPSILON );
idBlockAlloc<clipLink_t, 1024> clipLinkAllocator;
/*
===============================================================
idClipModel trace model cache
===============================================================
*/
static idList<trmCache_s*> traceModelCache;
static idHashIndex traceModelHash;
/*
===============
idClipModel::ClearTraceModelCache
===============
*/
void idClipModel::ClearTraceModelCache( void ) {
traceModelCache.DeleteContents( true );
traceModelHash.Free();
}
/*
===============
idClipModel::TraceModelCacheSize
===============
*/
int idClipModel::TraceModelCacheSize( void ) {
return traceModelCache.Num() * sizeof( idTraceModel );
}
/*
===============
idClipModel::AllocTraceModel
===============
*/
int idClipModel::AllocTraceModel( const idTraceModel &trm ) {
int i, hashKey, traceModelIndex;
trmCache_t *entry;
hashKey = GetTraceModelHashKey( trm );
for ( i = traceModelHash.First( hashKey ); i >= 0; i = traceModelHash.Next( i ) ) {
if ( traceModelCache[i]->trm == trm ) {
traceModelCache[i]->refCount++;
return i;
}
}
entry = new trmCache_t;
entry->trm = trm;
entry->trm.GetMassProperties( 1.0f, entry->volume, entry->centerOfMass, entry->inertiaTensor );
entry->refCount = 1;
traceModelIndex = traceModelCache.Append( entry );
traceModelHash.Add( hashKey, traceModelIndex );
return traceModelIndex;
}
/*
===============
idClipModel::FreeTraceModel
===============
*/
void idClipModel::FreeTraceModel( int traceModelIndex ) {
if ( traceModelIndex < 0 || traceModelIndex >= traceModelCache.Num() || traceModelCache[traceModelIndex]->refCount <= 0 ) {
gameLocal.Warning( "idClipModel::FreeTraceModel: tried to free uncached trace model" );
return;
}
traceModelCache[traceModelIndex]->refCount--;
}
/*
===============
idClipModel::GetCachedTraceModel
===============
*/
idTraceModel *idClipModel::GetCachedTraceModel( int traceModelIndex ) {
return &traceModelCache[traceModelIndex]->trm;
}
/*
===============
idClipModel::GetTraceModelHashKey
===============
*/
int idClipModel::GetTraceModelHashKey( const idTraceModel &trm ) {
const idVec3 &v = trm.bounds[0];
return ( trm.type << 8 ) ^ ( trm.numVerts << 4 ) ^ ( trm.numEdges << 2 ) ^ ( trm.numPolys << 0 ) ^ idMath::FloatHash( v.ToFloatPtr(), v.GetDimension() );
}
/*
===============
idClipModel::SaveTraceModels
===============
*/
void idClipModel::SaveTraceModels( idSaveGame *savefile ) {
int i;
savefile->WriteInt( traceModelCache.Num() );
for ( i = 0; i < traceModelCache.Num(); i++ ) {
trmCache_t *entry = traceModelCache[i];
savefile->WriteTraceModel( entry->trm );
savefile->WriteFloat( entry->volume );
savefile->WriteVec3( entry->centerOfMass );
savefile->WriteMat3( entry->inertiaTensor );
}
}
/*
===============
idClipModel::RestoreTraceModels
===============
*/
void idClipModel::RestoreTraceModels( idRestoreGame *savefile ) {
int i, num;
ClearTraceModelCache();
savefile->ReadInt( num );
traceModelCache.SetNum( num );
for ( i = 0; i < num; i++ ) {
trmCache_t *entry = new trmCache_t;
savefile->ReadTraceModel( entry->trm );
savefile->ReadFloat( entry->volume );
savefile->ReadVec3( entry->centerOfMass );
savefile->ReadMat3( entry->inertiaTensor );
entry->refCount = 0;
traceModelCache[i] = entry;
traceModelHash.Add( GetTraceModelHashKey( entry->trm ), i );
}
}
/*
===============================================================
idClipModel
===============================================================
*/
/*
================
idClipModel::LoadModel
================
*/
bool idClipModel::LoadModel( const char *name ) {
renderModelHandle = -1;
if ( traceModelIndex != -1 ) {
FreeTraceModel( traceModelIndex );
traceModelIndex = -1;
}
collisionModelHandle = collisionModelManager->LoadModel( name, false );
if ( collisionModelHandle ) {
collisionModelManager->GetModelBounds( collisionModelHandle, bounds );
collisionModelManager->GetModelContents( collisionModelHandle, contents );
return true;
} else {
bounds.Zero();
return false;
}
}
/*
================
idClipModel::LoadModel
================
*/
void idClipModel::LoadModel( const idTraceModel &trm ) {
collisionModelHandle = 0;
renderModelHandle = -1;
if ( traceModelIndex != -1 ) {
FreeTraceModel( traceModelIndex );
}
traceModelIndex = AllocTraceModel( trm );
bounds = trm.bounds;
}
/*
================
idClipModel::LoadModel
================
*/
void idClipModel::LoadModel( const int renderModelHandle ) {
collisionModelHandle = 0;
this->renderModelHandle = renderModelHandle;
if ( renderModelHandle != -1 ) {
const renderEntity_t *renderEntity = gameRenderWorld->GetRenderEntity( renderModelHandle );
if ( renderEntity ) {
bounds = renderEntity->bounds;
}
}
if ( traceModelIndex != -1 ) {
FreeTraceModel( traceModelIndex );
traceModelIndex = -1;
}
}
/*
================
idClipModel::Init
================
*/
void idClipModel::Init( void ) {
enabled = true;
entity = NULL;
id = 0;
owner = NULL;
origin.Zero();
axis.Identity();
bounds.Zero();
absBounds.Zero();
material = NULL;
contents = CONTENTS_BODY;
collisionModelHandle = 0;
renderModelHandle = -1;
traceModelIndex = -1;
clipLinks = NULL;
touchCount = -1;
}
/*
================
idClipModel::idClipModel
================
*/
idClipModel::idClipModel( void ) {
Init();
}
/*
================
idClipModel::idClipModel
================
*/
idClipModel::idClipModel( const char *name ) {
Init();
LoadModel( name );
}
/*
================
idClipModel::idClipModel
================
*/
idClipModel::idClipModel( const idTraceModel &trm ) {
Init();
LoadModel( trm );
}
/*
================
idClipModel::idClipModel
================
*/
idClipModel::idClipModel( const int renderModelHandle ) {
Init();
contents = CONTENTS_RENDERMODEL;
LoadModel( renderModelHandle );
}
/*
================
idClipModel::idClipModel
================
*/
idClipModel::idClipModel( const idClipModel *model ) {
enabled = model->enabled;
entity = model->entity;
id = model->id;
owner = model->owner;
origin = model->origin;
axis = model->axis;
bounds = model->bounds;
absBounds = model->absBounds;
material = model->material;
contents = model->contents;
collisionModelHandle = model->collisionModelHandle;
traceModelIndex = -1;
if ( model->traceModelIndex != -1 ) {
LoadModel( *GetCachedTraceModel( model->traceModelIndex ) );
}
renderModelHandle = model->renderModelHandle;
clipLinks = NULL;
touchCount = -1;
}
/*
================
idClipModel::~idClipModel
================
*/
idClipModel::~idClipModel( void ) {
// make sure the clip model is no longer linked
Unlink();
if ( traceModelIndex != -1 ) {
FreeTraceModel( traceModelIndex );
}
}
/*
================
idClipModel::Save
================
*/
void idClipModel::Save( idSaveGame *savefile ) const {
savefile->WriteBool( enabled );
savefile->WriteObject( entity );
savefile->WriteInt( id );
savefile->WriteObject( owner );
savefile->WriteVec3( origin );
savefile->WriteMat3( axis );
savefile->WriteBounds( bounds );
savefile->WriteBounds( absBounds );
savefile->WriteMaterial( material );
savefile->WriteInt( contents );
if ( collisionModelHandle >= 0 ) {
savefile->WriteString( collisionModelManager->GetModelName( collisionModelHandle ) );
} else {
savefile->WriteString( "" );
}
savefile->WriteInt( traceModelIndex );
savefile->WriteInt( renderModelHandle );
savefile->WriteBool( clipLinks != NULL );
savefile->WriteInt( touchCount );
}
/*
================
idClipModel::Restore
================
*/
void idClipModel::Restore( idRestoreGame *savefile ) {
idStr collisionModelName;
bool linked;
savefile->ReadBool( enabled );
savefile->ReadObject( reinterpret_cast<idClass *&>( entity ) );
savefile->ReadInt( id );
savefile->ReadObject( reinterpret_cast<idClass *&>( owner ) );
savefile->ReadVec3( origin );
savefile->ReadMat3( axis );
savefile->ReadBounds( bounds );
savefile->ReadBounds( absBounds );
savefile->ReadMaterial( material );
savefile->ReadInt( contents );
savefile->ReadString( collisionModelName );
if ( collisionModelName.Length() ) {
collisionModelHandle = collisionModelManager->LoadModel( collisionModelName, false );
} else {
collisionModelHandle = -1;
}
savefile->ReadInt( traceModelIndex );
if ( traceModelIndex >= 0 ) {
traceModelCache[traceModelIndex]->refCount++;
}
savefile->ReadInt( renderModelHandle );
savefile->ReadBool( linked );
savefile->ReadInt( touchCount );
// the render model will be set when the clip model is linked
renderModelHandle = -1;
clipLinks = NULL;
touchCount = -1;
if ( linked ) {
Link( gameLocal.clip, entity, id, origin, axis, renderModelHandle );
}
}
/*
================
idClipModel::SetPosition
================
*/
void idClipModel::SetPosition( const idVec3 &newOrigin, const idMat3 &newAxis ) {
if ( clipLinks ) {
Unlink(); // unlink from old position
}
origin = newOrigin;
axis = newAxis;
}
/*
================
idClipModel::Handle
================
*/
cmHandle_t idClipModel::Handle( void ) const {
assert( renderModelHandle == -1 );
if ( collisionModelHandle ) {
return collisionModelHandle;
} else if ( traceModelIndex != -1 ) {
return collisionModelManager->SetupTrmModel( *GetCachedTraceModel( traceModelIndex ), material );
} else {
// this happens in multiplayer on the combat models
gameLocal.Warning( "idClipModel::Handle: clip model %d on '%s' (%x) is not a collision or trace model", id, entity->name.c_str(), entity->entityNumber );
return 0;
}
}
/*
================
idClipModel::GetMassProperties
================
*/
void idClipModel::GetMassProperties( const float density, float &mass, idVec3 &centerOfMass, idMat3 &inertiaTensor ) const {
if ( traceModelIndex == -1 ) {
gameLocal.Error( "idClipModel::GetMassProperties: clip model %d on '%s' is not a trace model\n", id, entity->name.c_str() );
}
trmCache_t *entry = traceModelCache[traceModelIndex];
mass = entry->volume * density;
centerOfMass = entry->centerOfMass;
inertiaTensor = density * entry->inertiaTensor;
}
/*
===============
idClipModel::Unlink
===============
*/
void idClipModel::Unlink( void ) {
clipLink_t *link;
for ( link = clipLinks; link; link = clipLinks ) {
clipLinks = link->nextLink;
if ( link->prevInSector ) {
link->prevInSector->nextInSector = link->nextInSector;
} else {
link->sector->clipLinks = link->nextInSector;
}
if ( link->nextInSector ) {
link->nextInSector->prevInSector = link->prevInSector;
}
clipLinkAllocator.Free( link );
}
}
/*
===============
idClipModel::Link_r
===============
*/
void idClipModel::Link_r( struct clipSector_s *node ) {
clipLink_t *link;
while( node->axis != -1 ) {
if ( absBounds[0][node->axis] > node->dist ) {
node = node->children[0];
} else if ( absBounds[1][node->axis] < node->dist ) {
node = node->children[1];
} else {
Link_r( node->children[0] );
node = node->children[1];
}
}
link = clipLinkAllocator.Alloc();
link->clipModel = this;
link->sector = node;
link->nextInSector = node->clipLinks;
link->prevInSector = NULL;
if ( node->clipLinks ) {
node->clipLinks->prevInSector = link;
}
node->clipLinks = link;
link->nextLink = clipLinks;
clipLinks = link;
}
/*
===============
idClipModel::Link
===============
*/
void idClipModel::Link( idClip &clp ) {
assert( idClipModel::entity );
if ( !idClipModel::entity ) {
return;
}
if ( clipLinks ) {
Unlink(); // unlink from old position
}
if ( bounds.IsCleared() ) {
return;
}
// set the abs box
if ( axis.IsRotated() ) {
// expand for rotation
absBounds.FromTransformedBounds( bounds, origin, axis );
} else {
// normal
absBounds[0] = bounds[0] + origin;
absBounds[1] = bounds[1] + origin;
}
// because movement is clipped an epsilon away from an actual edge,
// we must fully check even when bounding boxes don't quite touch
absBounds[0] -= vec3_boxEpsilon;
absBounds[1] += vec3_boxEpsilon;
Link_r( clp.clipSectors );
}
/*
===============
idClipModel::Link
===============
*/
void idClipModel::Link( idClip &clp, idEntity *ent, int newId, const idVec3 &newOrigin, const idMat3 &newAxis, int renderModelHandle ) {
this->entity = ent;
this->id = newId;
this->origin = newOrigin;
this->axis = newAxis;
if ( renderModelHandle != -1 ) {
this->renderModelHandle = renderModelHandle;
const renderEntity_t *renderEntity = gameRenderWorld->GetRenderEntity( renderModelHandle );
if ( renderEntity ) {
this->bounds = renderEntity->bounds;
}
}
this->Link( clp );
}
/*
============
idClipModel::CheckModel
============
*/
cmHandle_t idClipModel::CheckModel( const char *name ) {
return collisionModelManager->LoadModel( name, false );
}
/*
===============================================================
idClip
===============================================================
*/
/*
===============
idClip::idClip
===============
*/
idClip::idClip( void ) {
numClipSectors = 0;
clipSectors = NULL;
worldBounds.Zero();
numRotations = numTranslations = numMotions = numRenderModelTraces = numContents = numContacts = 0;
}
/*
===============
idClip::CreateClipSectors_r
Builds a uniformly subdivided tree for the given world size
===============
*/
clipSector_t *idClip::CreateClipSectors_r( const int depth, const idBounds &bounds, idVec3 &maxSector ) {
int i;
clipSector_t *anode;
idVec3 size;
idBounds front, back;
anode = &clipSectors[idClip::numClipSectors];
idClip::numClipSectors++;
if ( depth == MAX_SECTOR_DEPTH ) {
anode->axis = -1;
anode->children[0] = anode->children[1] = NULL;
for ( i = 0; i < 3; i++ ) {
if ( bounds[1][i] - bounds[0][i] > maxSector[i] ) {
maxSector[i] = bounds[1][i] - bounds[0][i];
}
}
return anode;
}
size = bounds[1] - bounds[0];
if ( size[0] >= size[1] && size[0] >= size[2] ) {
anode->axis = 0;
} else if ( size[1] >= size[0] && size[1] >= size[2] ) {
anode->axis = 1;
} else {
anode->axis = 2;
}
anode->dist = 0.5f * ( bounds[1][anode->axis] + bounds[0][anode->axis] );
front = bounds;
back = bounds;
front[0][anode->axis] = back[1][anode->axis] = anode->dist;
anode->children[0] = CreateClipSectors_r( depth+1, front, maxSector );
anode->children[1] = CreateClipSectors_r( depth+1, back, maxSector );
return anode;
}
/*
===============
idClip::Init
===============
*/
void idClip::Init( void ) {
cmHandle_t h;
idVec3 size, maxSector = vec3_origin;
// clear clip sectors
clipSectors = new clipSector_t[MAX_SECTORS];
memset( clipSectors, 0, MAX_SECTORS * sizeof( clipSector_t ) );
numClipSectors = 0;
touchCount = -1;
// get world map bounds
h = collisionModelManager->LoadModel( "worldMap", false );
collisionModelManager->GetModelBounds( h, worldBounds );
// create world sectors
CreateClipSectors_r( 0, worldBounds, maxSector );
size = worldBounds[1] - worldBounds[0];
gameLocal.Printf( "map bounds are (%1.1f, %1.1f, %1.1f)\n", size[0], size[1], size[2] );
gameLocal.Printf( "max clip sector is (%1.1f, %1.1f, %1.1f)\n", maxSector[0], maxSector[1], maxSector[2] );
// initialize a default clip model
defaultClipModel.LoadModel( idTraceModel( idBounds( idVec3( 0, 0, 0 ) ).Expand( 8 ) ) );
// set counters to zero
numRotations = numTranslations = numMotions = numRenderModelTraces = numContents = numContacts = 0;
}
/*
===============
idClip::Shutdown
===============
*/
void idClip::Shutdown( void ) {
delete[] clipSectors;
clipSectors = NULL;
// free the trace model used for the temporaryClipModel
if ( temporaryClipModel.traceModelIndex != -1 ) {
idClipModel::FreeTraceModel( temporaryClipModel.traceModelIndex );
temporaryClipModel.traceModelIndex = -1;
}
// free the trace model used for the defaultClipModel
if ( defaultClipModel.traceModelIndex != -1 ) {
idClipModel::FreeTraceModel( defaultClipModel.traceModelIndex );
defaultClipModel.traceModelIndex = -1;
}
clipLinkAllocator.Shutdown();
}
/*
====================
idClip::ClipModelsTouchingBounds_r
====================
*/
typedef struct listParms_s {
idBounds bounds;
int contentMask;
idClipModel ** list;
int count;
int maxCount;
} listParms_t;
void idClip::ClipModelsTouchingBounds_r( const struct clipSector_s *node, listParms_t &parms ) const {
while( node->axis != -1 ) {
if ( parms.bounds[0][node->axis] > node->dist ) {
node = node->children[0];
} else if ( parms.bounds[1][node->axis] < node->dist ) {
node = node->children[1];
} else {
ClipModelsTouchingBounds_r( node->children[0], parms );
node = node->children[1];
}
}
for ( clipLink_t *link = node->clipLinks; link; link = link->nextInSector ) {
idClipModel *check = link->clipModel;
// if the clip model is enabled
if ( !check->enabled ) {
continue;
}
// avoid duplicates in the list
if ( check->touchCount == touchCount ) {
continue;
}
// if the clip model does not have any contents we are looking for
if ( !( check->contents & parms.contentMask ) ) {
continue;
}
// if the bounds really do overlap
if ( check->absBounds[0][0] > parms.bounds[1][0] ||
check->absBounds[1][0] < parms.bounds[0][0] ||
check->absBounds[0][1] > parms.bounds[1][1] ||
check->absBounds[1][1] < parms.bounds[0][1] ||
check->absBounds[0][2] > parms.bounds[1][2] ||
check->absBounds[1][2] < parms.bounds[0][2] ) {
continue;
}
if ( parms.count >= parms.maxCount ) {
gameLocal.Warning( "idClip::ClipModelsTouchingBounds_r: max count" );
return;
}
check->touchCount = touchCount;
parms.list[parms.count] = check;
parms.count++;
}
}
/*
================
idClip::ClipModelsTouchingBounds
================
*/
int idClip::ClipModelsTouchingBounds( const idBounds &bounds, int contentMask, idClipModel **clipModelList, int maxCount ) const {
listParms_t parms;
if ( bounds[0][0] > bounds[1][0] ||
bounds[0][1] > bounds[1][1] ||
bounds[0][2] > bounds[1][2] ) {
// we should not go through the tree for degenerate or backwards bounds
assert( false );
return 0;
}
parms.bounds[0] = bounds[0] - vec3_boxEpsilon;
parms.bounds[1] = bounds[1] + vec3_boxEpsilon;
parms.contentMask = contentMask;
parms.list = clipModelList;
parms.count = 0;
parms.maxCount = maxCount;
touchCount++;
ClipModelsTouchingBounds_r( clipSectors, parms );
return parms.count;
}
/*
================
idClip::EntitiesTouchingBounds
================
*/
int idClip::EntitiesTouchingBounds( const idBounds &bounds, int contentMask, idEntity **entityList, int maxCount ) const {
idClipModel *clipModelList[MAX_GENTITIES];
int i, j, count, entCount;
count = idClip::ClipModelsTouchingBounds( bounds, contentMask, clipModelList, MAX_GENTITIES );
entCount = 0;
for ( i = 0; i < count; i++ ) {
// entity could already be in the list because an entity can use multiple clip models
for ( j = 0; j < entCount; j++ ) {
if ( entityList[j] == clipModelList[i]->entity ) {
break;
}
}
if ( j >= entCount ) {
if ( entCount >= maxCount ) {
gameLocal.Warning( "idClip::EntitiesTouchingBounds: max count" );
return entCount;
}
entityList[entCount] = clipModelList[i]->entity;
entCount++;
}
}
return entCount;
}
/*
====================
idClip::GetTraceClipModels
an ent will be excluded from testing if:
cm->entity == passEntity ( don't clip against the pass entity )
cm->entity == passOwner ( missiles don't clip with owner )
cm->owner == passEntity ( don't interact with your own missiles )
cm->owner == passOwner ( don't interact with other missiles from same owner )
====================
*/
int idClip::GetTraceClipModels( const idBounds &bounds, int contentMask, const idEntity *passEntity, idClipModel **clipModelList ) const {
int i, num;
idClipModel *cm;
idEntity *passOwner;
num = ClipModelsTouchingBounds( bounds, contentMask, clipModelList, MAX_GENTITIES );
if ( !passEntity ) {
return num;
}
if ( passEntity->GetPhysics()->GetNumClipModels() > 0 ) {
passOwner = passEntity->GetPhysics()->GetClipModel()->GetOwner();
} else {
passOwner = NULL;
}
for ( i = 0; i < num; i++ ) {
cm = clipModelList[i];
// check if we should ignore this entity
if ( cm->entity == passEntity ) {
clipModelList[i] = NULL; // don't clip against the pass entity
} else if ( cm->entity == passOwner ) {
clipModelList[i] = NULL; // missiles don't clip with their owner
} else if ( cm->owner ) {
if ( cm->owner == passEntity ) {
clipModelList[i] = NULL; // don't clip against own missiles
} else if ( cm->owner == passOwner ) {
clipModelList[i] = NULL; // don't clip against other missiles from same owner
}
}
}
return num;
}
/*
============
idClip::TraceRenderModel
============
*/
void idClip::TraceRenderModel( trace_t &trace, const idVec3 &start, const idVec3 &end, const float radius, const idMat3 &axis, idClipModel *touch ) const {
trace.fraction = 1.0f;
// if the trace is passing through the bounds
if ( touch->absBounds.Expand( radius ).LineIntersection( start, end ) ) {
modelTrace_t modelTrace;
// test with exact render model and modify trace_t structure accordingly
if ( gameRenderWorld->ModelTrace( modelTrace, touch->renderModelHandle, start, end, radius ) ) {
trace.fraction = modelTrace.fraction;
trace.endAxis = axis;
trace.endpos = modelTrace.point;
trace.c.normal = modelTrace.normal;
trace.c.dist = modelTrace.point * modelTrace.normal;
trace.c.point = modelTrace.point;
trace.c.type = CONTACT_TRMVERTEX;
trace.c.modelFeature = 0;
trace.c.trmFeature = 0;
trace.c.contents = modelTrace.material->GetContentFlags();
trace.c.material = modelTrace.material;
// NOTE: trace.c.id will be the joint number
touch->id = JOINT_HANDLE_TO_CLIPMODEL_ID( modelTrace.jointNumber );
}
}
}
/*
============
idClip::TraceModelForClipModel
============
*/
const idTraceModel *idClip::TraceModelForClipModel( const idClipModel *mdl ) const {
if ( !mdl ) {
return NULL;
} else {
if ( !mdl->IsTraceModel() ) {
if ( mdl->GetEntity() ) {
gameLocal.Error( "TraceModelForClipModel: clip model %d on '%s' is not a trace model\n", mdl->GetId(), mdl->GetEntity()->name.c_str() );
} else {
gameLocal.Error( "TraceModelForClipModel: clip model %d is not a trace model\n", mdl->GetId() );
}
}
return idClipModel::GetCachedTraceModel( mdl->traceModelIndex );
}
}
/*
============
idClip::TestHugeTranslation
============
*/
ID_INLINE bool TestHugeTranslation( trace_t &results, const idClipModel *mdl, const idVec3 &start, const idVec3 &end, const idMat3 &trmAxis ) {
if ( mdl != NULL && ( end - start ).LengthSqr() > Square( CM_MAX_TRACE_DIST ) ) {
results.fraction = 0.0f;
results.endpos = start;
results.endAxis = trmAxis;
memset( &results.c, 0, sizeof( results.c ) );
results.c.point = start;
if ( mdl->GetEntity() ) {
gameLocal.Printf( "huge translation for clip model %d on entity %d '%s'\n", mdl->GetId(), mdl->GetEntity()->entityNumber, mdl->GetEntity()->GetName() );
} else {
gameLocal.Printf( "huge translation for clip model %d\n", mdl->GetId() );
}
gameLocal.Printf( " from (%.2f %.2f %.2f) to (%.2f %.2f %.2f)\n", start.x, start.y, start.z, end.x, end.y, end.z);
#ifndef CTF
// May be important: This occurs in CTF when a player connects and spawns
// in the PVS of a player that has a flag that is spawning the idMoveableItem
// "nuggets". The error seems benign and the assert was getting in the way
// of testing.
assert( 0 );
#endif
return true;
}
return false;
}
/*
============
idClip::TranslationEntities
============
*/
void idClip::TranslationEntities( trace_t &results, const idVec3 &start, const idVec3 &end,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask, const idEntity *passEntity ) {
int i, num;
idClipModel *touch, *clipModelList[MAX_GENTITIES];
idBounds traceBounds;
float radius;
trace_t trace;
const idTraceModel *trm;
if ( TestHugeTranslation( results, mdl, start, end, trmAxis ) ) {
return;
}
trm = TraceModelForClipModel( mdl );
results.fraction = 1.0f;
results.endpos = end;
results.endAxis = trmAxis;
if ( !trm ) {
traceBounds.FromPointTranslation( start, end - start );
radius = 0.0f;
} else {
traceBounds.FromBoundsTranslation( trm->bounds, start, trmAxis, end - start );
radius = trm->bounds.GetRadius();
}
num = GetTraceClipModels( traceBounds, contentMask, passEntity, clipModelList );
for ( i = 0; i < num; i++ ) {
touch = clipModelList[i];
if ( !touch ) {
continue;
}
if ( touch->renderModelHandle != -1 ) {
idClip::numRenderModelTraces++;
TraceRenderModel( trace, start, end, radius, trmAxis, touch );
} else {
idClip::numTranslations++;
collisionModelManager->Translation( &trace, start, end, trm, trmAxis, contentMask,
touch->Handle(), touch->origin, touch->axis );
}
if ( trace.fraction < results.fraction ) {
results = trace;
results.c.entityNum = touch->entity->entityNumber;
results.c.id = touch->id;
if ( results.fraction == 0.0f ) {
break;
}
}
}
}
/*
============
idClip::Translation
============
*/
bool idClip::Translation( trace_t &results, const idVec3 &start, const idVec3 &end,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask, const idEntity *passEntity ) {
int i, num;
idClipModel *touch, *clipModelList[MAX_GENTITIES];
idBounds traceBounds;
float radius;
trace_t trace;
const idTraceModel *trm;
if ( TestHugeTranslation( results, mdl, start, end, trmAxis ) ) {
return true;
}
trm = TraceModelForClipModel( mdl );
if ( !passEntity || passEntity->entityNumber != ENTITYNUM_WORLD ) {
// test world
idClip::numTranslations++;
collisionModelManager->Translation( &results, start, end, trm, trmAxis, contentMask, 0, vec3_origin, mat3_default );
results.c.entityNum = results.fraction != 1.0f ? ENTITYNUM_WORLD : ENTITYNUM_NONE;
if ( results.fraction == 0.0f ) {
return true; // blocked immediately by the world
}
} else {
memset( &results, 0, sizeof( results ) );
results.fraction = 1.0f;
results.endpos = end;
results.endAxis = trmAxis;
}
if ( !trm ) {
traceBounds.FromPointTranslation( start, results.endpos - start );
radius = 0.0f;
} else {
traceBounds.FromBoundsTranslation( trm->bounds, start, trmAxis, results.endpos - start );
radius = trm->bounds.GetRadius();
}
num = GetTraceClipModels( traceBounds, contentMask, passEntity, clipModelList );
for ( i = 0; i < num; i++ ) {
touch = clipModelList[i];
if ( !touch ) {
continue;
}
if ( touch->renderModelHandle != -1 ) {
idClip::numRenderModelTraces++;
TraceRenderModel( trace, start, end, radius, trmAxis, touch );
} else {
idClip::numTranslations++;
collisionModelManager->Translation( &trace, start, end, trm, trmAxis, contentMask,
touch->Handle(), touch->origin, touch->axis );
}
if ( trace.fraction < results.fraction ) {
results = trace;
results.c.entityNum = touch->entity->entityNumber;
results.c.id = touch->id;
if ( results.fraction == 0.0f ) {
break;
}
}
}
return ( results.fraction < 1.0f );
}
/*
============
idClip::Rotation
============
*/
bool idClip::Rotation( trace_t &results, const idVec3 &start, const idRotation &rotation,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask, const idEntity *passEntity ) {
int i, num;
idClipModel *touch, *clipModelList[MAX_GENTITIES];
idBounds traceBounds;
trace_t trace;
const idTraceModel *trm;
trm = TraceModelForClipModel( mdl );
if ( !passEntity || passEntity->entityNumber != ENTITYNUM_WORLD ) {
// test world
idClip::numRotations++;
collisionModelManager->Rotation( &results, start, rotation, trm, trmAxis, contentMask, 0, vec3_origin, mat3_default );
results.c.entityNum = results.fraction != 1.0f ? ENTITYNUM_WORLD : ENTITYNUM_NONE;
if ( results.fraction == 0.0f ) {
return true; // blocked immediately by the world
}
} else {
memset( &results, 0, sizeof( results ) );
results.fraction = 1.0f;
results.endpos = start;
results.endAxis = trmAxis * rotation.ToMat3();
}
if ( !trm ) {
traceBounds.FromPointRotation( start, rotation );
} else {
traceBounds.FromBoundsRotation( trm->bounds, start, trmAxis, rotation );
}
num = GetTraceClipModels( traceBounds, contentMask, passEntity, clipModelList );
for ( i = 0; i < num; i++ ) {
touch = clipModelList[i];
if ( !touch ) {
continue;
}
// no rotational collision with render models
if ( touch->renderModelHandle != -1 ) {
continue;
}
idClip::numRotations++;
collisionModelManager->Rotation( &trace, start, rotation, trm, trmAxis, contentMask,
touch->Handle(), touch->origin, touch->axis );
if ( trace.fraction < results.fraction ) {
results = trace;
results.c.entityNum = touch->entity->entityNumber;
results.c.id = touch->id;
if ( results.fraction == 0.0f ) {
break;
}
}
}
return ( results.fraction < 1.0f );
}
/*
============
idClip::Motion
============
*/
bool idClip::Motion( trace_t &results, const idVec3 &start, const idVec3 &end, const idRotation &rotation,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask, const idEntity *passEntity ) {
int i, num;
idClipModel *touch, *clipModelList[MAX_GENTITIES];
idVec3 dir, endPosition;
idBounds traceBounds;
float radius;
trace_t translationalTrace, rotationalTrace, trace;
idRotation endRotation;
const idTraceModel *trm;
assert( rotation.GetOrigin() == start );
if ( TestHugeTranslation( results, mdl, start, end, trmAxis ) ) {
return true;
}
if ( mdl != NULL && rotation.GetAngle() != 0.0f && rotation.GetVec() != vec3_origin ) {
// if no translation
if ( start == end ) {
// pure rotation
return Rotation( results, start, rotation, mdl, trmAxis, contentMask, passEntity );
}
} else if ( start != end ) {
// pure translation
return Translation( results, start, end, mdl, trmAxis, contentMask, passEntity );
} else {
// no motion
results.fraction = 1.0f;
results.endpos = start;
results.endAxis = trmAxis;
return false;
}
trm = TraceModelForClipModel( mdl );
radius = trm->bounds.GetRadius();
if ( !passEntity || passEntity->entityNumber != ENTITYNUM_WORLD ) {
// translational collision with world
idClip::numTranslations++;
collisionModelManager->Translation( &translationalTrace, start, end, trm, trmAxis, contentMask, 0, vec3_origin, mat3_default );
translationalTrace.c.entityNum = translationalTrace.fraction != 1.0f ? ENTITYNUM_WORLD : ENTITYNUM_NONE;
} else {
memset( &translationalTrace, 0, sizeof( translationalTrace ) );
translationalTrace.fraction = 1.0f;
translationalTrace.endpos = end;
translationalTrace.endAxis = trmAxis;
}
if ( translationalTrace.fraction != 0.0f ) {
traceBounds.FromBoundsRotation( trm->bounds, start, trmAxis, rotation );
dir = translationalTrace.endpos - start;
for ( i = 0; i < 3; i++ ) {
if ( dir[i] < 0.0f ) {
traceBounds[0][i] += dir[i];
}
else {
traceBounds[1][i] += dir[i];
}
}
num = GetTraceClipModels( traceBounds, contentMask, passEntity, clipModelList );
for ( i = 0; i < num; i++ ) {
touch = clipModelList[i];
if ( !touch ) {
continue;
}
if ( touch->renderModelHandle != -1 ) {
idClip::numRenderModelTraces++;
TraceRenderModel( trace, start, end, radius, trmAxis, touch );
} else {
idClip::numTranslations++;
collisionModelManager->Translation( &trace, start, end, trm, trmAxis, contentMask,
touch->Handle(), touch->origin, touch->axis );
}
if ( trace.fraction < translationalTrace.fraction ) {
translationalTrace = trace;
translationalTrace.c.entityNum = touch->entity->entityNumber;
translationalTrace.c.id = touch->id;
if ( translationalTrace.fraction == 0.0f ) {
break;
}
}
}
} else {
num = -1;
}
endPosition = translationalTrace.endpos;
endRotation = rotation;
endRotation.SetOrigin( endPosition );
if ( !passEntity || passEntity->entityNumber != ENTITYNUM_WORLD ) {
// rotational collision with world
idClip::numRotations++;
collisionModelManager->Rotation( &rotationalTrace, endPosition, endRotation, trm, trmAxis, contentMask, 0, vec3_origin, mat3_default );
rotationalTrace.c.entityNum = rotationalTrace.fraction != 1.0f ? ENTITYNUM_WORLD : ENTITYNUM_NONE;
} else {
memset( &rotationalTrace, 0, sizeof( rotationalTrace ) );
rotationalTrace.fraction = 1.0f;
rotationalTrace.endpos = endPosition;
rotationalTrace.endAxis = trmAxis * rotation.ToMat3();
}
if ( rotationalTrace.fraction != 0.0f ) {
if ( num == -1 ) {
traceBounds.FromBoundsRotation( trm->bounds, endPosition, trmAxis, endRotation );
num = GetTraceClipModels( traceBounds, contentMask, passEntity, clipModelList );
}
for ( i = 0; i < num; i++ ) {
touch = clipModelList[i];
if ( !touch ) {
continue;
}
// no rotational collision detection with render models
if ( touch->renderModelHandle != -1 ) {
continue;
}
idClip::numRotations++;
collisionModelManager->Rotation( &trace, endPosition, endRotation, trm, trmAxis, contentMask,
touch->Handle(), touch->origin, touch->axis );
if ( trace.fraction < rotationalTrace.fraction ) {
rotationalTrace = trace;
rotationalTrace.c.entityNum = touch->entity->entityNumber;
rotationalTrace.c.id = touch->id;
if ( rotationalTrace.fraction == 0.0f ) {
break;
}
}
}
}
if ( rotationalTrace.fraction < 1.0f ) {
results = rotationalTrace;
} else {
results = translationalTrace;
results.endAxis = rotationalTrace.endAxis;
}
results.fraction = Max( translationalTrace.fraction, rotationalTrace.fraction );
return ( translationalTrace.fraction < 1.0f || rotationalTrace.fraction < 1.0f );
}
/*
============
idClip::Contacts
============
*/
int idClip::Contacts( contactInfo_t *contacts, const int maxContacts, const idVec3 &start, const idVec6 &dir, const float depth,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask, const idEntity *passEntity ) {
int i, j, num, n, numContacts;
idClipModel *touch, *clipModelList[MAX_GENTITIES];
idBounds traceBounds;
const idTraceModel *trm;
trm = TraceModelForClipModel( mdl );
if ( !passEntity || passEntity->entityNumber != ENTITYNUM_WORLD ) {
// test world
idClip::numContacts++;
numContacts = collisionModelManager->Contacts( contacts, maxContacts, start, dir, depth, trm, trmAxis, contentMask, 0, vec3_origin, mat3_default );
} else {
numContacts = 0;
}
for ( i = 0; i < numContacts; i++ ) {
contacts[i].entityNum = ENTITYNUM_WORLD;
contacts[i].id = 0;
}
if ( numContacts >= maxContacts ) {
return numContacts;
}
if ( !trm ) {
traceBounds = idBounds( start ).Expand( depth );
} else {
traceBounds.FromTransformedBounds( trm->bounds, start, trmAxis );
traceBounds.ExpandSelf( depth );
}
num = GetTraceClipModels( traceBounds, contentMask, passEntity, clipModelList );
for ( i = 0; i < num; i++ ) {
touch = clipModelList[i];
if ( !touch ) {
continue;
}
// no contacts with render models
if ( touch->renderModelHandle != -1 ) {
continue;
}
idClip::numContacts++;
n = collisionModelManager->Contacts( contacts + numContacts, maxContacts - numContacts,
start, dir, depth, trm, trmAxis, contentMask,
touch->Handle(), touch->origin, touch->axis );
for ( j = 0; j < n; j++ ) {
contacts[numContacts].entityNum = touch->entity->entityNumber;
contacts[numContacts].id = touch->id;
numContacts++;
}
if ( numContacts >= maxContacts ) {
break;
}
}
return numContacts;
}
/*
============
idClip::Contents
============
*/
int idClip::Contents( const idVec3 &start, const idClipModel *mdl, const idMat3 &trmAxis, int contentMask, const idEntity *passEntity ) {
int i, num, contents;
idClipModel *touch, *clipModelList[MAX_GENTITIES];
idBounds traceBounds;
const idTraceModel *trm;
trm = TraceModelForClipModel( mdl );
if ( !passEntity || passEntity->entityNumber != ENTITYNUM_WORLD ) {
// test world
idClip::numContents++;
contents = collisionModelManager->Contents( start, trm, trmAxis, contentMask, 0, vec3_origin, mat3_default );
} else {
contents = 0;
}
if ( !trm ) {
traceBounds[0] = start;
traceBounds[1] = start;
} else if ( trmAxis.IsRotated() ) {
traceBounds.FromTransformedBounds( trm->bounds, start, trmAxis );
} else {
traceBounds[0] = trm->bounds[0] + start;
traceBounds[1] = trm->bounds[1] + start;
}
num = GetTraceClipModels( traceBounds, -1, passEntity, clipModelList );
for ( i = 0; i < num; i++ ) {
touch = clipModelList[i];
if ( !touch ) {
continue;
}
// no contents test with render models
if ( touch->renderModelHandle != -1 ) {
continue;
}
// if the entity does not have any contents we are looking for
if ( ( touch->contents & contentMask ) == 0 ) {
continue;
}
// if the entity has no new contents flags
if ( ( touch->contents & contents ) == touch->contents ) {
continue;
}
idClip::numContents++;
if ( collisionModelManager->Contents( start, trm, trmAxis, contentMask, touch->Handle(), touch->origin, touch->axis ) ) {
contents |= ( touch->contents & contentMask );
}
}
return contents;
}
/*
============
idClip::TranslationModel
============
*/
void idClip::TranslationModel( trace_t &results, const idVec3 &start, const idVec3 &end,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask,
cmHandle_t model, const idVec3 &modelOrigin, const idMat3 &modelAxis ) {
const idTraceModel *trm = TraceModelForClipModel( mdl );
idClip::numTranslations++;
collisionModelManager->Translation( &results, start, end, trm, trmAxis, contentMask, model, modelOrigin, modelAxis );
}
/*
============
idClip::RotationModel
============
*/
void idClip::RotationModel( trace_t &results, const idVec3 &start, const idRotation &rotation,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask,
cmHandle_t model, const idVec3 &modelOrigin, const idMat3 &modelAxis ) {
const idTraceModel *trm = TraceModelForClipModel( mdl );
idClip::numRotations++;
collisionModelManager->Rotation( &results, start, rotation, trm, trmAxis, contentMask, model, modelOrigin, modelAxis );
}
/*
============
idClip::ContactsModel
============
*/
int idClip::ContactsModel( contactInfo_t *contacts, const int maxContacts, const idVec3 &start, const idVec6 &dir, const float depth,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask,
cmHandle_t model, const idVec3 &modelOrigin, const idMat3 &modelAxis ) {
const idTraceModel *trm = TraceModelForClipModel( mdl );
idClip::numContacts++;
return collisionModelManager->Contacts( contacts, maxContacts, start, dir, depth, trm, trmAxis, contentMask, model, modelOrigin, modelAxis );
}
/*
============
idClip::ContentsModel
============
*/
int idClip::ContentsModel( const idVec3 &start,
const idClipModel *mdl, const idMat3 &trmAxis, int contentMask,
cmHandle_t model, const idVec3 &modelOrigin, const idMat3 &modelAxis ) {
const idTraceModel *trm = TraceModelForClipModel( mdl );
idClip::numContents++;
return collisionModelManager->Contents( start, trm, trmAxis, contentMask, model, modelOrigin, modelAxis );
}
/*
============
idClip::GetModelContactFeature
============
*/
bool idClip::GetModelContactFeature( const contactInfo_t &contact, const idClipModel *clipModel, idFixedWinding &winding ) const {
int i;
cmHandle_t handle;
idVec3 start, end;
handle = -1;
winding.Clear();
if ( clipModel == NULL ) {
handle = 0;
} else {
if ( clipModel->renderModelHandle != -1 ) {
winding += contact.point;
return true;
} else if ( clipModel->traceModelIndex != -1 ) {
handle = collisionModelManager->SetupTrmModel( *idClipModel::GetCachedTraceModel( clipModel->traceModelIndex ), clipModel->material );
} else {
handle = clipModel->collisionModelHandle;
}
}
// if contact with a collision model
if ( handle != -1 ) {
switch( contact.type ) {
case CONTACT_EDGE: {
// the model contact feature is a collision model edge
collisionModelManager->GetModelEdge( handle, contact.modelFeature, start, end );
winding += start;
winding += end;
break;
}
case CONTACT_MODELVERTEX: {
// the model contact feature is a collision model vertex
collisionModelManager->GetModelVertex( handle, contact.modelFeature, start );
winding += start;
break;
}
case CONTACT_TRMVERTEX: {
// the model contact feature is a collision model polygon
collisionModelManager->GetModelPolygon( handle, contact.modelFeature, winding );
break;
}
}
}
// transform the winding to world space
if ( clipModel ) {
for ( i = 0; i < winding.GetNumPoints(); i++ ) {
winding[i].ToVec3() *= clipModel->axis;
winding[i].ToVec3() += clipModel->origin;
}
}
return true;
}
/*
============
idClip::PrintStatistics
============
*/
void idClip::PrintStatistics( void ) {
gameLocal.Printf( "t = %-3d, r = %-3d, m = %-3d, render = %-3d, contents = %-3d, contacts = %-3d\n",
numTranslations, numRotations, numMotions, numRenderModelTraces, numContents, numContacts );
numRotations = numTranslations = numMotions = numRenderModelTraces = numContents = numContacts = 0;
}
/*
============
idClip::DrawClipModels
============
*/
void idClip::DrawClipModels( const idVec3 &eye, const float radius, const idEntity *passEntity ) {
int i, num;
idBounds bounds;
idClipModel *clipModelList[MAX_GENTITIES];
idClipModel *clipModel;
bounds = idBounds( eye ).Expand( radius );
num = idClip::ClipModelsTouchingBounds( bounds, -1, clipModelList, MAX_GENTITIES );
for ( i = 0; i < num; i++ ) {
clipModel = clipModelList[i];
if ( clipModel->GetEntity() == passEntity ) {
continue;
}
if ( clipModel->renderModelHandle != -1 ) {
gameRenderWorld->DebugBounds( colorCyan, clipModel->GetAbsBounds() );
} else {
collisionModelManager->DrawModel( clipModel->Handle(), clipModel->GetOrigin(), clipModel->GetAxis(), eye, radius );
}
}
}
/*
============
idClip::DrawModelContactFeature
============
*/
bool idClip::DrawModelContactFeature( const contactInfo_t &contact, const idClipModel *clipModel, int lifetime ) const {
int i;
idMat3 axis;
idFixedWinding winding;
if ( !GetModelContactFeature( contact, clipModel, winding ) ) {
return false;
}
axis = contact.normal.ToMat3();
if ( winding.GetNumPoints() == 1 ) {
gameRenderWorld->DebugLine( colorCyan, winding[0].ToVec3(), winding[0].ToVec3() + 2.0f * axis[0], lifetime );
gameRenderWorld->DebugLine( colorWhite, winding[0].ToVec3() - 1.0f * axis[1], winding[0].ToVec3() + 1.0f * axis[1], lifetime );
gameRenderWorld->DebugLine( colorWhite, winding[0].ToVec3() - 1.0f * axis[2], winding[0].ToVec3() + 1.0f * axis[2], lifetime );
} else {
for ( i = 0; i < winding.GetNumPoints(); i++ ) {
gameRenderWorld->DebugLine( colorCyan, winding[i].ToVec3(), winding[(i+1)%winding.GetNumPoints()].ToVec3(), lifetime );
}
}
axis[0] = -axis[0];
axis[2] = -axis[2];
gameRenderWorld->DrawText( contact.material->GetName(), winding.GetCenter() - 4.0f * axis[2], 0.1f, colorWhite, axis, 1, 5000 );
return true;
}