Paranoia2/pm_shared/trace.cpp

567 lines
15 KiB
C++

/*
trace.cpp - trace triangle meshes
Copyright (C) 2012 Uncle Mike
This program 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.
This program 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.
*/
//#include "windows.h"
#include <alert.h>
#include "vector.h"
#include "matrix.h"
#include "const.h"
#include "com_model.h"
#include "trace.h"
#include "mathlib.h"
#ifdef CLIENT_DLL
#include "cl_dll.h"
#include "render_api.h"
#else
#include "edict.h"
#include "eiface.h"
#include "physcallback.h"
#endif
#include "enginecallback.h"
void TraceMesh :: SetTraceMesh( mmesh_t *cached_mesh, areanode_t *tree, int modelindex )
{
m_pModel = (model_t *)MODEL_HANDLE( modelindex );
mesh = cached_mesh;
areanodes = tree;
}
mstudiomaterial_t *TraceMesh :: GetMaterialForFacet( const mfacet_t *facet )
{
if( !m_pModel ) return NULL;
mstudiomaterial_t *materials = m_pModel->materials;
studiohdr_t *phdr = (studiohdr_t *)m_pModel->cache.data;
if( !materials || !phdr ) return NULL;
short *pskinref = (short *)((byte *)phdr + phdr->skinindex);
if( m_iSkin > 0 && m_iSkin < phdr->numskinfamilies )
pskinref += (m_iSkin * phdr->numskinref);
return &materials[pskinref[facet->skinref]];
}
mstudiotexture_t *TraceMesh :: GetTextureForFacet( const mfacet_t *facet )
{
mstudiomaterial_t *material = GetMaterialForFacet( facet );
if( material )
return material->pSource;
return NULL;
}
bool TraceMesh :: IsTrans( const mfacet_t *facet )
{
mstudiotexture_t *texture = GetTextureForFacet( facet );
return (texture && FBitSet( texture->flags, STUDIO_NF_MASKED ));
}
void CheckAngles( Vector &angles )
{
// blackmagic to avoid invalid signbits state
if( angles.y != 0.0f && fmod( angles.y, 45.0f ) == 0.0f )
angles.y += 0.01f;
}
void TraceMesh :: SetupTrace( const Vector &start, const Vector &mins, const Vector &maxs, const Vector &end, trace_t *tr )
{
trace = tr;
memset( trace, 0, sizeof( *trace ));
trace->fraction = m_flRealFraction = 1.0f;
Vector lmins = mins, lmaxs = maxs, offset;
float t, halfwidth, halfheight;
int i, total_signbits = 0;
m_vecSrcStart = start;
m_vecSrcEnd = end;
// adjust so that mins and maxs are always symetric, which
// avoids some complications with plane expanding of rotated
// bmodels
for( i = 0; i < 3; i++ )
{
offset[i] = ( mins[i] + maxs[i] ) * 0.5f;
lmins[i] = mins[i] - offset[i];
lmaxs[i] = maxs[i] - offset[i];
m_vecSrcStart[i] = start[i] + offset[i];
m_vecSrcEnd[i] = end[i] + offset[i];
}
// HACKHACK: shrink bbox a bit...
if( mins != maxs )
ExpandBounds( lmins, lmaxs, -(1.0f/8.0f));
halfwidth = lmaxs[0];
halfheight = lmaxs[2];
bUseCapsule = (offset == g_vecZero) ? false : true;
m_flSphereRadius = ( halfwidth > halfheight ) ? halfheight : halfwidth;
t = halfheight - m_flSphereRadius;
CheckAngles( m_vecAngles );
// inverse pitch because of stupid quake bug
m_transform = matrix4x4( m_vecOrigin, Vector( -m_vecAngles.x, m_vecAngles.y, m_vecAngles.z ), m_vecScale ).InvertFull();
m_vecStart = m_transform.VectorTransform( m_vecSrcStart );
m_vecEnd = m_transform.VectorTransform( m_vecSrcEnd );
if( mins != maxs )
{
// compute a full bounding box
for( i = 0; i < 8; i++ )
{
Vector p1, p2;
p1.x = ( i & 1 ) ? lmins[0] : lmaxs[0];
p1.y = ( i & 2 ) ? lmins[1] : lmaxs[1];
p1.z = ( i & 4 ) ? lmins[2] : lmaxs[2];
p2 = m_transform.VectorRotate( p1 );
// NOTE: this is looks silly but it works for some reasons:
// bbox are symetric and stored in local space
// signbits are detected normals for bbox side tests
int j = SignbitsForPlane( -p2 );
m_vecOffsets[j] = p2;
total_signbits += j;
}
if( total_signbits != 28 )
{
ALERT( at_error, "total signbits %d != 28 (mins %g %g %g) maxs( %g %g %g)\n",
total_signbits, lmins.x, lmins.y, lmins.z, lmaxs.x, lmaxs.y, lmaxs.z );
ALERT( at_error, "rotated angles %g %g %g\n", -m_vecAngles.x, m_vecAngles.y, m_vecAngles.z );
for( i = 0; i < 8; i++ )
{
Vector p1, p2;
p1.x = ( i & 1 ) ? lmins[0] : lmaxs[0];
p1.y = ( i & 2 ) ? lmins[1] : lmaxs[1];
p1.z = ( i & 4 ) ? lmins[2] : lmaxs[2];
p2 = m_transform.VectorRotate( p1 );
// NOTE: this is looks silly but it works for some reasons:
// bbox are symetric and stored in local space
// signbits are detected normals for bbox side tests
int j = SignbitsForPlane( -p2 );
Msg( "#%i (%g %g %g) -> (%g %g %g), signbits %d\n", i, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, j );
}
}
}
else
{
// just reset offsets
memset( m_vecOffsets, 0, sizeof( m_vecOffsets ));
}
// rotated sphere offset for capsule
m_flSphereOffset[0] = m_transform[2][0] * t;
m_flSphereOffset[1] = -m_transform[2][1] * t;
m_flSphereOffset[2] = m_transform[2][2] * t;
// now calculate the local bbox
ClearBounds( lmins, lmaxs );
for( i = 0; i < 8; i++ )
AddPointToBounds( m_vecOffsets[i], lmins, lmaxs );
material = NULL;
m_vecTraceDirection = m_vecEnd - m_vecStart;
m_flTraceDistance = m_vecTraceDirection.Length();
m_vecTraceDirection = m_vecTraceDirection.Normalize();
// build a bounding box of the entire move
ClearBounds( m_vecAbsMins, m_vecAbsMaxs );
AddPointToBounds( m_vecStart + lmins, m_vecAbsMins, m_vecAbsMaxs );
AddPointToBounds( m_vecStart + lmaxs, m_vecAbsMins, m_vecAbsMaxs );
AddPointToBounds( m_vecEnd + lmins, m_vecAbsMins, m_vecAbsMaxs );
AddPointToBounds( m_vecEnd + lmaxs, m_vecAbsMins, m_vecAbsMaxs );
// spread min\max by a pixel
for( i = 0; i < 3; i++ )
{
m_vecAbsMins[i] -= 1.0f;
m_vecAbsMaxs[i] += 1.0f;
}
// use untransformed values to avoid FP rounding errors
if( mins == maxs )
bIsTraceLine = true;
else bIsTraceLine = false;
if( start == end )
bIsTestPosition = true;
else bIsTestPosition = false;
}
void TraceMesh :: ClipBoxToFacet( mfacet_t *facet )
{
mplane_t *p, *clipplane;
float enterfrac, leavefrac, distfrac;
mstudiotexture_t *ptexture;
bool getout, startout;
Vector startp, endp;
float d, d1, d2, f;
if( !facet->numplanes )
return;
ptexture = GetTextureForFacet( facet );
if( !bIsTraceLine && ptexture )
{
if( FBitSet( ptexture->flags, STUDIO_NF_MASKED ) && !FBitSet( ptexture->flags, STUDIO_NF_ALPHASOLID ))
return;
}
enterfrac = -1.0f;
leavefrac = 1.0f;
clipplane = NULL;
checkcount++;
getout = false;
startout = false;
for( int i = 0; i < facet->numplanes; i++ )
{
p = &mesh->planes[facet->indices[i]];
if( bUseCapsule )
{
// adjust the plane distance apropriately for radius
float dist = p->dist + m_flSphereRadius;
// find the closest point on the capsule to the plane
float t = DotProduct( p->normal, m_flSphereOffset );
if( t > 0.0f )
{
startp = m_vecStart - m_flSphereOffset;
endp = m_vecEnd - m_flSphereOffset;
}
else
{
startp = m_vecStart + m_flSphereOffset;
endp = m_vecEnd + m_flSphereOffset;
}
d1 = DotProduct( startp, p->normal ) - dist;
d2 = DotProduct( endp, p->normal ) - dist;
}
else
{
// adjust the plane distance apropriately for mins/maxs
float dist = p->dist - DotProduct( m_vecOffsets[p->signbits], p->normal );
d1 = DotProduct( m_vecStart, p->normal ) - dist;
d2 = DotProduct( m_vecEnd, p->normal ) - dist;
}
if( d2 > 0.0f ) getout = true; // endpoint is not in solid
if( d1 > 0.0f ) startout = true;
// if completely in front of face, no intersection
if( d1 > 0 && d2 >= d1 )
return;
if( d1 <= 0 && d2 <= 0 )
continue;
// crosses face
d = 1.0f / (d1 - d2);
f = d1 * d;
if( d > 0.0f )
{
// enter
if( f > enterfrac )
{
distfrac = d;
enterfrac = f;
clipplane = p;
}
}
else if( d < 0.0f )
{
// leave
if( f < leavefrac )
leavefrac = f;
}
}
if( !startout )
{
// original point was inside brush
trace->startsolid = true;
if( !getout ) trace->allsolid = true;
return;
}
if( enterfrac - FRAC_EPSILON <= leavefrac )
{
if( enterfrac > -1.0f && enterfrac < m_flRealFraction )
{
if( enterfrac < 0 ) enterfrac = 0;
m_flRealFraction = enterfrac;
trace->plane.normal = clipplane->normal;
trace->plane.dist = clipplane->dist;
trace->fraction = enterfrac - DIST_EPSILON * distfrac;
material = GetMaterialForFacet( facet ); // material was hit
}
}
}
void TraceMesh :: TestBoxInFacet( mfacet_t *facet )
{
mstudiotexture_t *ptexture;
Vector startp;
float d1;
mplane_t *p;
if( !facet->numplanes )
return;
ptexture = GetTextureForFacet( facet );
if( !bIsTraceLine && ptexture )
{
if( FBitSet( ptexture->flags, STUDIO_NF_MASKED ) && !FBitSet( ptexture->flags, STUDIO_NF_ALPHASOLID ))
return;
}
checkcount++;
for( int i = 0; i < facet->numplanes; i++ )
{
p = &mesh->planes[facet->indices[i]];
if( bUseCapsule )
{
// adjust the plane distance apropriately for radius
float dist = p->dist + m_flSphereRadius;
// find the closest point on the capsule to the plane
float t = DotProduct( p->normal, m_flSphereOffset );
if( t > 0.0f ) startp = m_vecStart - m_flSphereOffset;
else startp = m_vecStart + m_flSphereOffset;
d1 = DotProduct( startp, p->normal ) - dist;
}
else
{
// adjust the plane distance apropriately for mins/maxs
float dist = p->dist - DotProduct( m_vecOffsets[p->signbits], p->normal );
d1 = DotProduct( m_vecStart, p->normal ) - dist;
}
// if completely in front of face, no intersection
if( d1 > 0 ) return;
}
// inside this brush
m_flRealFraction = 0.0f;
trace->startsolid = true;
trace->allsolid = true;
}
bool TraceMesh :: ClipRayToFacet( const mfacet_t *facet )
{
// begin calculating determinant - also used to calculate u parameter
Vector pvec = CrossProduct( m_vecTraceDirection, facet->edge2 );
// if determinant is near zero, trace lies in plane of triangle
float det = DotProduct( facet->edge1, pvec );
// the non-culling branch
if( fabs( det ) < COPLANAR_EPSILON )
return false;
checkcount++;
Vector n = CrossProduct( facet->edge2, facet->edge1 );
Vector p = m_vecEnd - m_vecStart;
// calculate distance from first vertex to ray origin
Vector tvec = m_vecStart - facet->triangle[0].point;
float d1 = -DotProduct( n, tvec );
float d2 = DotProduct( n, p );
if( fabs( d2 ) < 0.0001 )
return false;
// get intersect point of ray with triangle plane
float frac = d1 / d2;
if( frac <= 0.0f ) return false;
if( frac > m_flRealFraction )
return false; // we have hit something earlier
float invDet = 1.0f / det;
bool force_solid = false;
// calculate u parameter and test bounds
float u = DotProduct( tvec, pvec ) * invDet;
if( u < -BARY_EPSILON || u > ( 1.0f + BARY_EPSILON ))
return false;
// prepare to test v parameter
Vector qvec = CrossProduct( tvec, facet->edge1 );
// calculate v parameter and test bounds
float v = DotProduct( m_vecTraceDirection, qvec ) * invDet;
if( v < -BARY_EPSILON || ( u + v ) > ( 1.0f + BARY_EPSILON ))
return false;
// calculate t (depth)
float depth = DotProduct( facet->edge2, qvec ) * invDet;
if( depth <= 0.001f || depth >= m_flTraceDistance )
return false;
n = n.Normalize();
mstudiotexture_t *ptexture = GetTextureForFacet( facet );
if( ptexture && !FBitSet( ptexture->flags, STUDIO_NF_MASKED ))
force_solid = true;
#ifdef CLIENT_DLL
// FIXME: how to handle trace flags on the client?
#else
if( FBitSet( gpGlobals->trace_flags, FTRACE_IGNORE_ALPHATEST ))
force_solid = true;
#endif
// most surfaces are completely opaque
if( !ptexture || !ptexture->index || force_solid )
{
trace->fraction = m_flRealFraction = frac;
material = GetMaterialForFacet( facet ); // material was hit
if( DotProduct( m_vecTraceDirection, n ) >= 0.0f )
trace->plane.normal = -n;
else trace->plane.normal = n;
return true;
}
// try to avoid double shadows near triangle seams
if( u < -ASLF_EPSILON || u > ( 1.0f + ASLF_EPSILON ) || v < -ASLF_EPSILON || ( u + v ) > ( 1.0f + ASLF_EPSILON ))
return false;
// calculate w parameter
float w = 1.0f - ( u + v );
// calculate st from uvw (barycentric) coordinates
float s = w * facet->triangle[0].st[0] + u * facet->triangle[1].st[0] + v * facet->triangle[2].st[0];
float t = w * facet->triangle[0].st[1] + u * facet->triangle[1].st[1] + v * facet->triangle[2].st[1];
s = s - floor( s );
t = t - floor( t );
int is = s * ptexture->width;
int it = t * ptexture->height;
if( is < 0 ) is = 0;
if( it < 0 ) it = 0;
if( is > ptexture->width - 1 )
is = ptexture->width - 1;
if( it > ptexture->height - 1 )
it = ptexture->height - 1;
byte *pixels = (byte *)GET_TEXTURE_DATA( ptexture->index );
// test pixel
if( pixels && pixels[it * ptexture->width + is] == 0xFF )
return false; // last color in palette is indicated alpha-pixel
trace->fraction = m_flRealFraction = frac;
material = GetMaterialForFacet( facet ); // material was hit
if( DotProduct( m_vecTraceDirection, n ) >= 0.0f )
trace->plane.normal = -n;
else trace->plane.normal = n;
return true;
}
void TraceMesh :: ClipToLinks( areanode_t *node )
{
link_t *l, *next;
mfacet_t *facet;
// touch linked edicts
for( l = node->solid_edicts.next; l != &node->solid_edicts; l = next )
{
next = l->next;
facet = FACET_FROM_AREA( l );
if( !BoundsIntersect( m_vecAbsMins, m_vecAbsMaxs, facet->mins, facet->maxs ))
continue;
// might intersect, so do an exact clip
if( !m_flRealFraction ) return;
if( bIsTestPosition )
TestBoxInFacet( facet );
else if( bIsTraceLine )
ClipRayToFacet( facet );
else ClipBoxToFacet( facet );
}
// recurse down both sides
if( node->axis == -1 ) return;
if( m_vecAbsMaxs[node->axis] > node->dist )
ClipToLinks( node->children[0] );
if( m_vecAbsMins[node->axis] < node->dist )
ClipToLinks( node->children[1] );
}
bool TraceMesh :: DoTrace( void )
{
if( !mesh || !BoundsIntersect( mesh->mins, mesh->maxs, m_vecAbsMins, m_vecAbsMaxs ))
return false; // invalid mesh or no intersection
checkcount = 0;
if( areanodes )
{
ClipToLinks( areanodes );
}
else
{
mfacet_t *facet = mesh->facets;
for( int i = 0; i < mesh->numfacets; i++, facet++ )
{
if( bIsTestPosition )
TestBoxInFacet( facet );
else if( bIsTraceLine )
ClipRayToFacet( facet );
else ClipBoxToFacet( facet );
if( !m_flRealFraction )
break;
}
}
// ALERT( at_aiconsole, "total %i checks for %s\n", checkcount, areanodes ? "tree" : "brute force" );
trace->plane.normal = m_transform.VectorIRotate( trace->plane.normal ).Normalize();
trace->fraction = bound( 0.0f, trace->fraction, 1.0f );
if( trace->fraction == 1.0f ) trace->endpos = m_vecSrcEnd;
else VectorLerp( m_vecSrcStart, trace->fraction, m_vecSrcEnd, trace->endpos );
trace->plane.dist = DotProduct( trace->endpos, trace->plane.normal );
return (trace->fraction != 1.0f);
}