mirror of
https://github.com/TorqueGameEngines/Torque3D.git
synced 2026-01-19 20:24:49 +00:00
1792 lines
55 KiB
C++
1792 lines
55 KiB
C++
//-----------------------------------------------------------------------------
|
|
// Copyright (c) 2012 GarageGames, LLC
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to
|
|
// deal in the Software without restriction, including without limitation the
|
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
// sell copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
// IN THE SOFTWARE.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#include "platform/platform.h"
|
|
#include "T3D/decal/decalManager.h"
|
|
|
|
#include "scene/sceneManager.h"
|
|
#include "scene/sceneRenderState.h"
|
|
#include "ts/tsShapeInstance.h"
|
|
#include "console/console.h"
|
|
#include "console/dynamicTypes.h"
|
|
#include "gfx/primBuilder.h"
|
|
#include "console/consoleTypes.h"
|
|
#include "platform/profiler.h"
|
|
#include "gfx/gfxTransformSaver.h"
|
|
#include "lighting/lightManager.h"
|
|
#include "lighting/lightInfo.h"
|
|
#include "gfx/gfxDrawUtil.h"
|
|
#include "gfx/sim/gfxStateBlockData.h"
|
|
#include "materials/shaderData.h"
|
|
#include "materials/matInstance.h"
|
|
#include "renderInstance/renderPassManager.h"
|
|
#include "core/resourceManager.h"
|
|
#include "core/stream/fileStream.h"
|
|
#include "gfx/gfxDebugEvent.h"
|
|
#include "math/util/quadTransforms.h"
|
|
#include "math/mathUtils.h"
|
|
#include "core/volume.h"
|
|
#include "core/module.h"
|
|
#include "T3D/decal/decalData.h"
|
|
#include "console/engineAPI.h"
|
|
|
|
|
|
extern bool gEditingMission;
|
|
|
|
|
|
MODULE_BEGIN( DecalManager )
|
|
|
|
MODULE_INIT_AFTER( Scene )
|
|
MODULE_SHUTDOWN_BEFORE( Scene )
|
|
|
|
MODULE_INIT
|
|
{
|
|
gDecalManager = new DecalManager;
|
|
gClientSceneGraph->addObjectToScene( gDecalManager );
|
|
}
|
|
|
|
MODULE_SHUTDOWN
|
|
{
|
|
gClientSceneGraph->removeObjectFromScene( gDecalManager );
|
|
SAFE_DELETE( gDecalManager );
|
|
}
|
|
|
|
MODULE_END;
|
|
|
|
|
|
/// A bias applied to the nearPlane for Decal and DecalRoad rendering.
|
|
/// Is set by by LevelInfo.
|
|
F32 gDecalBias = 0.0015f;
|
|
|
|
bool DecalManager::smDecalsOn = true;
|
|
bool DecalManager::smDebugRender = false;
|
|
F32 DecalManager::smDecalLifeTimeScale = 1.0f;
|
|
bool DecalManager::smPoolBuffers = true;
|
|
const U32 DecalManager::smMaxVerts = 6000;
|
|
const U32 DecalManager::smMaxIndices = 10000;
|
|
|
|
DecalManager *gDecalManager = NULL;
|
|
|
|
IMPLEMENT_CONOBJECT(DecalManager);
|
|
|
|
ConsoleDoc(
|
|
"@defgroup Decals\n"
|
|
"@brief Decals are non-SimObject derived objects that are stored and loaded "
|
|
"separately from the normal mission file.\n\n"
|
|
|
|
"The DecalManager handles all aspects of decal management including loading, "
|
|
"creation, saving, and automatically deleting decals that have exceeded their "
|
|
"lifeSpan.\n\n"
|
|
|
|
"The static decals associated with a mission are normally loaded immediately "
|
|
"after the mission itself has loaded as shown below.\n"
|
|
|
|
"@tsexample\n"
|
|
"// Load the static mission decals.\n"
|
|
"decalManagerLoad( %missionName @ \".decals\" );\n"
|
|
"@endtsexample\n"
|
|
|
|
"@ingroup FX\n"
|
|
);
|
|
|
|
ConsoleDocClass( DecalManager,
|
|
"@brief The object that manages all of the decals in the active mission.\n\n"
|
|
"@see Decals\n"
|
|
"@ingroup Decals\n"
|
|
"@ingroup FX\n"
|
|
);
|
|
|
|
namespace {
|
|
|
|
S32 QSORT_CALLBACK cmpDecalInstance(const void* p1, const void* p2)
|
|
{
|
|
const DecalInstance** pd1 = (const DecalInstance**)p1;
|
|
const DecalInstance** pd2 = (const DecalInstance**)p2;
|
|
|
|
return int(((char *)(*pd1)->mDataBlock) - ((char *)(*pd2)->mDataBlock));
|
|
}
|
|
|
|
S32 QSORT_CALLBACK cmpPointsXY( const void *p1, const void *p2 )
|
|
{
|
|
const Point3F *pnt1 = (const Point3F*)p1;
|
|
const Point3F *pnt2 = (const Point3F*)p2;
|
|
|
|
if ( pnt1->x < pnt2->x )
|
|
return -1;
|
|
else if ( pnt1->x > pnt2->x )
|
|
return 1;
|
|
else if ( pnt1->y < pnt2->y )
|
|
return -1;
|
|
else if ( pnt1->y > pnt2->y )
|
|
return 1;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
S32 QSORT_CALLBACK cmpQuadPointTheta( const void *p1, const void *p2 )
|
|
{
|
|
const Point4F *pnt1 = (const Point4F*)p1;
|
|
const Point4F *pnt2 = (const Point4F*)p2;
|
|
|
|
if ( mFabs( pnt1->w ) > mFabs( pnt2->w ) )
|
|
return 1;
|
|
else if ( mFabs( pnt1->w ) < mFabs( pnt2->w ) )
|
|
return -1;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
static Point3F gSortPoint;
|
|
|
|
S32 QSORT_CALLBACK cmpDecalDistance( const void *p1, const void *p2 )
|
|
{
|
|
const DecalInstance** pd1 = (const DecalInstance**)p1;
|
|
const DecalInstance** pd2 = (const DecalInstance**)p2;
|
|
|
|
F32 dist1 = ( (*pd1)->mPosition - gSortPoint ).lenSquared();
|
|
F32 dist2 = ( (*pd2)->mPosition - gSortPoint ).lenSquared();
|
|
|
|
return mSign( dist1 - dist2 );
|
|
}
|
|
|
|
S32 QSORT_CALLBACK cmpDecalRenderOrder( const void *p1, const void *p2 )
|
|
{
|
|
const DecalInstance** pd1 = (const DecalInstance**)p1;
|
|
const DecalInstance** pd2 = (const DecalInstance**)p2;
|
|
|
|
if ( ( (*pd2)->mFlags & SaveDecal ) && !( (*pd1)->mFlags & SaveDecal ) )
|
|
return -1;
|
|
else if ( !( (*pd2)->mFlags & SaveDecal ) && ( (*pd1)->mFlags & SaveDecal ) )
|
|
return 1;
|
|
else
|
|
{
|
|
S32 priority = (*pd1)->getRenderPriority() - (*pd2)->getRenderPriority();
|
|
|
|
if ( priority != 0 )
|
|
return priority;
|
|
|
|
if ( (*pd2)->mFlags & SaveDecal )
|
|
{
|
|
S32 id = ( (*pd1)->mDataBlock->getMaterial()->getId() - (*pd2)->mDataBlock->getMaterial()->getId() );
|
|
if ( id != 0 )
|
|
return id;
|
|
|
|
return (*pd1)->mCreateTime - (*pd2)->mCreateTime;
|
|
}
|
|
else
|
|
return (*pd1)->mCreateTime - (*pd2)->mCreateTime;
|
|
}
|
|
}
|
|
|
|
} // namespace {}
|
|
|
|
// These numbers should be tweaked to get as many dynamically placed decals
|
|
// as possible to allocate buffer arrays with the FreeListChunker.
|
|
enum
|
|
{
|
|
SIZE_CLASS_0 = 256,
|
|
SIZE_CLASS_1 = 512,
|
|
SIZE_CLASS_2 = 1024,
|
|
|
|
NUM_SIZE_CLASSES = 3
|
|
};
|
|
|
|
//-------------------------------------------------------------------------
|
|
// DecalManager
|
|
//-------------------------------------------------------------------------
|
|
DecalManager::DecalManager()
|
|
{
|
|
#ifdef DECALMANAGER_DEBUG
|
|
VECTOR_SET_ASSOCIATION( mDebugPlanes );
|
|
#endif
|
|
|
|
setGlobalBounds();
|
|
|
|
mDataFileName = NULL;
|
|
|
|
mTypeMask |= EnvironmentObjectType;
|
|
|
|
mDirty = false;
|
|
|
|
mChunkers[0] = new FreeListChunkerUntyped( SIZE_CLASS_0 * sizeof( U8 ) );
|
|
mChunkers[1] = new FreeListChunkerUntyped( SIZE_CLASS_1 * sizeof( U8 ) );
|
|
mChunkers[2] = new FreeListChunkerUntyped( SIZE_CLASS_2 * sizeof( U8 ) );
|
|
|
|
GFXDevice::getDeviceEventSignal().notify(this, &DecalManager::_handleGFXEvent);
|
|
}
|
|
|
|
DecalManager::~DecalManager()
|
|
{
|
|
GFXDevice::getDeviceEventSignal().remove(this, &DecalManager::_handleGFXEvent);
|
|
|
|
clearData();
|
|
|
|
for( U32 i = 0; i < NUM_SIZE_CLASSES; ++ i )
|
|
delete mChunkers[ i ];
|
|
}
|
|
|
|
void DecalManager::consoleInit()
|
|
{
|
|
Con::addVariable( "$pref::Decals::enabled", TypeBool, &smDecalsOn,
|
|
"Controls whether decals are rendered.\n"
|
|
"@ingroup Decals" );
|
|
|
|
Con::addVariable( "$pref::Decals::lifeTimeScale", TypeF32, &smDecalLifeTimeScale,
|
|
"@brief Lifetime that decals will last after being created in the world.\n"
|
|
"Deprecated. Use DecalData::lifeSpan instead.\n"
|
|
"@ingroup Decals" );
|
|
|
|
Con::addVariable( "$Decals::poolBuffers", TypeBool, &smPoolBuffers,
|
|
"If true, will merge all PrimitiveBuffers and VertexBuffers into a pair "
|
|
"of pools before clearing them at the end of a frame.\n"
|
|
"If false, will just clear them at the end of a frame.\n"
|
|
"@ingroup Decals" );
|
|
|
|
Con::addVariable( "$Decals::debugRender", TypeBool, &smDebugRender,
|
|
"If true, the decal spheres will be visualized when in the editor.\n\n"
|
|
"@ingroup Decals" );
|
|
|
|
Con::addVariable( "$Decals::sphereDistanceTolerance", TypeF32, &DecalSphere::smDistanceTolerance,
|
|
"The distance at which the decal system will start breaking up decal "
|
|
"spheres when adding new decals.\n\n"
|
|
"@ingroup Decals" );
|
|
|
|
Con::addVariable( "$Decals::sphereRadiusTolerance", TypeF32, &DecalSphere::smRadiusTolerance,
|
|
"The radius beyond which the decal system will start breaking up decal "
|
|
"spheres when adding new decals.\n\n"
|
|
"@ingroup Decals" );
|
|
}
|
|
|
|
bool DecalManager::_handleGFXEvent(GFXDevice::GFXDeviceEventType event)
|
|
{
|
|
switch(event)
|
|
{
|
|
case GFXDevice::deEndOfFrame:
|
|
|
|
// Return PrimitiveBuffers and VertexBuffers used this frame to the pool.
|
|
|
|
if ( smPoolBuffers )
|
|
{
|
|
mPBPool.merge( mPBs );
|
|
mPBs.clear();
|
|
|
|
mVBPool.merge( mVBs );
|
|
mVBs.clear();
|
|
}
|
|
else
|
|
{
|
|
_freePools();
|
|
}
|
|
|
|
break;
|
|
|
|
default: ;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DecalManager::clipDecal( DecalInstance *decal, Vector<Point3F> *edgeVerts, const Point2F *clipDepth )
|
|
{
|
|
PROFILE_SCOPE( DecalManager_clipDecal );
|
|
|
|
// Free old verts and indices.
|
|
_freeBuffers( decal );
|
|
|
|
F32 halfSize = decal->mSize * 0.5f;
|
|
|
|
// Ugly hack for ProjectedShadow!
|
|
F32 halfSizeZ = clipDepth ? clipDepth->x : halfSize;
|
|
F32 negHalfSize = clipDepth ? clipDepth->y : halfSize;
|
|
Point3F decalHalfSize( halfSize, halfSize, halfSize );
|
|
Point3F decalHalfSizeZ( halfSizeZ, halfSizeZ, halfSizeZ );
|
|
|
|
MatrixF projMat( true );
|
|
decal->getWorldMatrix( &projMat );
|
|
|
|
const VectorF &crossVec = decal->mNormal;
|
|
const Point3F &decalPos = decal->mPosition;
|
|
|
|
VectorF newFwd, newRight;
|
|
projMat.getColumn( 0, &newRight );
|
|
projMat.getColumn( 1, &newFwd );
|
|
|
|
VectorF objRight( 1.0f, 0, 0 );
|
|
VectorF objFwd( 0, 1.0f, 0 );
|
|
VectorF objUp( 0, 0, 1.0f );
|
|
|
|
// See above re: decalHalfSizeZ hack.
|
|
mClipper.clear();
|
|
mClipper.mPlaneList.setSize(6);
|
|
mClipper.mPlaneList[0].set( ( decalPos + ( -newRight * halfSize ) ), -newRight );
|
|
mClipper.mPlaneList[1].set( ( decalPos + ( -newFwd * halfSize ) ), -newFwd );
|
|
mClipper.mPlaneList[2].set( ( decalPos + ( -crossVec * decalHalfSizeZ ) ), -crossVec );
|
|
mClipper.mPlaneList[3].set( ( decalPos + ( newRight * halfSize ) ), newRight );
|
|
mClipper.mPlaneList[4].set( ( decalPos + ( newFwd * halfSize ) ), newFwd );
|
|
mClipper.mPlaneList[5].set( ( decalPos + ( crossVec * negHalfSize ) ), crossVec );
|
|
|
|
mClipper.mNormal = decal->mNormal;
|
|
|
|
const DecalData *decalData = decal->mDataBlock;
|
|
|
|
mClipper.mNormalTolCosineRadians = mCos( mDegToRad( decalData->clippingAngle ) );
|
|
|
|
Box3F box( -decalHalfSizeZ, decalHalfSizeZ );
|
|
|
|
projMat.mul( box );
|
|
|
|
PROFILE_START( DecalManager_clipDecal_buildPolyList );
|
|
getContainer()->buildPolyList( PLC_Decal, box, decalData->clippingMasks, &mClipper );
|
|
PROFILE_END();
|
|
|
|
mClipper.cullUnusedVerts();
|
|
mClipper.triangulate();
|
|
|
|
const U32 numVerts = mClipper.mVertexList.size();
|
|
const U32 numIndices = mClipper.mIndexList.size();
|
|
|
|
if ( !numVerts || !numIndices )
|
|
return false;
|
|
|
|
// Fail if either of the buffer metrics exceeds our limits
|
|
// on dynamic geometry buffers.
|
|
if ( numVerts > smMaxVerts ||
|
|
numIndices > smMaxIndices )
|
|
return false;
|
|
|
|
if ( !decalData->skipVertexNormals )
|
|
mClipper.generateNormals();
|
|
|
|
#ifdef DECALMANAGER_DEBUG
|
|
mDebugPlanes.clear();
|
|
mDebugPlanes.merge( mClipper.mPlaneList );
|
|
#endif
|
|
|
|
decal->mVertCount = numVerts;
|
|
decal->mIndxCount = numIndices;
|
|
|
|
Vector<Point3F> tmpPoints;
|
|
|
|
tmpPoints.push_back(( objFwd * decalHalfSize ) + ( objRight * decalHalfSize ));
|
|
tmpPoints.push_back(( objFwd * decalHalfSize ) + ( -objRight * decalHalfSize ));
|
|
tmpPoints.push_back(( -objFwd * decalHalfSize ) + ( -objRight * decalHalfSize ));
|
|
|
|
Point3F lowerLeft(( -objFwd * decalHalfSize ) + ( objRight * decalHalfSize ));
|
|
|
|
projMat.inverse();
|
|
|
|
_generateWindingOrder( lowerLeft, &tmpPoints );
|
|
|
|
BiQuadToSqr quadToSquare( Point2F( lowerLeft.x, lowerLeft.y ),
|
|
Point2F( tmpPoints[0].x, tmpPoints[0].y ),
|
|
Point2F( tmpPoints[1].x, tmpPoints[1].y ),
|
|
Point2F( tmpPoints[2].x, tmpPoints[2].y ) );
|
|
|
|
Point2F uv( 0, 0 );
|
|
Point3F vecX(0.0f, 0.0f, 0.0f);
|
|
|
|
// Allocate memory for vert and index arrays
|
|
_allocBuffers( decal );
|
|
|
|
// Mark this so that the color will be assigned on these verts the next
|
|
// time it renders, since we just threw away the previous verts.
|
|
decal->mLastAlpha = -1;
|
|
|
|
Point3F vertPoint( 0, 0, 0 );
|
|
|
|
for ( U32 i = 0; i < mClipper.mVertexList.size(); i++ )
|
|
{
|
|
const ClippedPolyList::Vertex &vert = mClipper.mVertexList[i];
|
|
vertPoint = vert.point;
|
|
|
|
// Transform this point to
|
|
// object space to look up the
|
|
// UV coordinate for this vertex.
|
|
projMat.mulP( vertPoint );
|
|
|
|
// Clamp the point to be within the quad.
|
|
vertPoint.x = mClampF( vertPoint.x, -decalHalfSize.x, decalHalfSize.x );
|
|
vertPoint.y = mClampF( vertPoint.y, -decalHalfSize.y, decalHalfSize.y );
|
|
|
|
// Get our UV.
|
|
uv = quadToSquare.transform( Point2F( vertPoint.x, vertPoint.y ) );
|
|
|
|
const RectF &rect = decal->mDataBlock->texRect[decal->mTextureRectIdx];
|
|
|
|
uv *= rect.extent;
|
|
uv += rect.point;
|
|
|
|
// Set the world space vertex position.
|
|
decal->mVerts[i].point = vert.point;
|
|
|
|
decal->mVerts[i].texCoord.set( uv.x, uv.y );
|
|
|
|
if ( mClipper.mNormalList.empty() )
|
|
continue;
|
|
|
|
decal->mVerts[i].normal = mClipper.mNormalList[i];
|
|
decal->mVerts[i].normal.normalize();
|
|
|
|
if( mFabs( decal->mVerts[i].normal.z ) > 0.8f )
|
|
mCross( decal->mVerts[i].normal, Point3F( 1.0f, 0.0f, 0.0f ), &vecX );
|
|
else if ( mFabs( decal->mVerts[i].normal.x ) > 0.8f )
|
|
mCross( decal->mVerts[i].normal, Point3F( 0.0f, 1.0f, 0.0f ), &vecX );
|
|
else if ( mFabs( decal->mVerts[i].normal.y ) > 0.8f )
|
|
mCross( decal->mVerts[i].normal, Point3F( 0.0f, 0.0f, 1.0f ), &vecX );
|
|
|
|
decal->mVerts[i].tangent = mCross( decal->mVerts[i].normal, vecX );
|
|
}
|
|
|
|
U32 curIdx = 0;
|
|
for ( U32 j = 0; j < mClipper.mPolyList.size(); j++ )
|
|
{
|
|
// Write indices for each Poly
|
|
ClippedPolyList::Poly *poly = &mClipper.mPolyList[j];
|
|
|
|
AssertFatal( poly->vertexCount == 3, "Got non-triangle poly!" );
|
|
|
|
decal->mIndices[curIdx] = mClipper.mIndexList[poly->vertexStart];
|
|
curIdx++;
|
|
decal->mIndices[curIdx] = mClipper.mIndexList[poly->vertexStart + 1];
|
|
curIdx++;
|
|
decal->mIndices[curIdx] = mClipper.mIndexList[poly->vertexStart + 2];
|
|
curIdx++;
|
|
}
|
|
|
|
if ( !edgeVerts )
|
|
return true;
|
|
|
|
Point3F tmpHullPt( 0, 0, 0 );
|
|
Vector<Point3F> tmpHullPts;
|
|
|
|
for ( U32 i = 0; i < mClipper.mVertexList.size(); i++ )
|
|
{
|
|
const ClippedPolyList::Vertex &vert = mClipper.mVertexList[i];
|
|
tmpHullPt = vert.point;
|
|
projMat.mulP( tmpHullPt );
|
|
tmpHullPts.push_back( tmpHullPt );
|
|
}
|
|
|
|
edgeVerts->clear();
|
|
U32 verts = _generateConvexHull( tmpHullPts, edgeVerts );
|
|
edgeVerts->setSize( verts );
|
|
|
|
projMat.inverse();
|
|
for ( U32 i = 0; i < edgeVerts->size(); i++ )
|
|
projMat.mulP( (*edgeVerts)[i] );
|
|
|
|
return true;
|
|
}
|
|
|
|
DecalInstance* DecalManager::addDecal( const Point3F &pos,
|
|
const Point3F &normal,
|
|
F32 rotAroundNormal,
|
|
DecalData *decalData,
|
|
F32 decalScale,
|
|
S32 decalTexIndex,
|
|
U8 flags )
|
|
{
|
|
MatrixF mat( true );
|
|
MathUtils::getMatrixFromUpVector( normal, &mat );
|
|
|
|
AngAxisF rot( normal, rotAroundNormal );
|
|
MatrixF rotmat;
|
|
rot.setMatrix( &rotmat );
|
|
mat.mul( rotmat );
|
|
|
|
Point3F tangent;
|
|
mat.getColumn( 1, &tangent );
|
|
|
|
return addDecal( pos, normal, tangent, decalData, decalScale, decalTexIndex, flags );
|
|
}
|
|
|
|
DecalInstance* DecalManager::addDecal( const Point3F& pos,
|
|
const Point3F& normal,
|
|
const Point3F& tangent,
|
|
DecalData* decalData,
|
|
F32 decalScale,
|
|
S32 decalTexIndex,
|
|
U8 flags )
|
|
{
|
|
if ( !mData && !_createDataFile() )
|
|
return NULL;
|
|
|
|
// only dirty the manager if this decal should be saved
|
|
if ( flags & SaveDecal )
|
|
mDirty = true;
|
|
|
|
return mData->addDecal( pos, normal, tangent, decalData, decalScale, decalTexIndex, flags );
|
|
}
|
|
|
|
void DecalManager::removeDecal( DecalInstance *inst )
|
|
{
|
|
// If this is a decal we save then we need
|
|
// to set the dirty flag.
|
|
if ( inst->mFlags & SaveDecal )
|
|
mDirty = true;
|
|
|
|
// Remove the decal from the instance vector.
|
|
|
|
if( inst->mId != -1 && inst->mId < mDecalInstanceVec.size() )
|
|
mDecalInstanceVec[ inst->mId ] = NULL;
|
|
|
|
// Release its geometry (if it has any).
|
|
|
|
_freeBuffers( inst );
|
|
|
|
// Remove it from the decal file.
|
|
|
|
if ( mData )
|
|
mData->removeDecal( inst );
|
|
}
|
|
|
|
DecalInstance* DecalManager::getDecal( S32 id )
|
|
{
|
|
if( id < 0 || id >= mDecalInstanceVec.size() )
|
|
return NULL;
|
|
|
|
return mDecalInstanceVec[id];
|
|
}
|
|
|
|
void DecalManager::notifyDecalModified( DecalInstance *inst )
|
|
{
|
|
// If this is a decal we save then we need
|
|
// to set the dirty flag.
|
|
if ( inst->mFlags & SaveDecal )
|
|
mDirty = true;
|
|
|
|
if ( mData )
|
|
mData->notifyDecalModified( inst );
|
|
}
|
|
|
|
DecalInstance* DecalManager::getClosestDecal( const Point3F &pos )
|
|
{
|
|
if ( !mData )
|
|
return NULL;
|
|
|
|
const Vector<DecalSphere*> &grid = mData->getSphereList();
|
|
|
|
DecalInstance *inst = NULL;
|
|
SphereF worldPickSphere( pos, 0.5f );
|
|
SphereF worldInstSphere( Point3F( 0, 0, 0 ), 1.0f );
|
|
|
|
Vector<DecalInstance*> collectedInsts;
|
|
|
|
for ( U32 i = 0; i < grid.size(); i++ )
|
|
{
|
|
DecalSphere *decalSphere = grid[i];
|
|
const SphereF &worldSphere = decalSphere->mWorldSphere;
|
|
if ( !worldSphere.isIntersecting( worldPickSphere ) &&
|
|
!worldSphere.isContained( pos ) )
|
|
continue;
|
|
|
|
const Vector<DecalInstance*> &items = decalSphere->mItems;
|
|
for ( U32 n = 0; n < items.size(); n++ )
|
|
{
|
|
inst = items[n];
|
|
if ( !inst )
|
|
continue;
|
|
|
|
worldInstSphere.center = inst->mPosition;
|
|
worldInstSphere.radius = inst->mSize;
|
|
|
|
if ( !worldInstSphere.isContained( inst->mPosition ) )
|
|
continue;
|
|
|
|
collectedInsts.push_back( inst );
|
|
}
|
|
}
|
|
|
|
F32 closestDistance = F32_MAX;
|
|
F32 currentDist = 0;
|
|
U32 closestIndex = 0;
|
|
for ( U32 i = 0; i < collectedInsts.size(); i++ )
|
|
{
|
|
inst = collectedInsts[i];
|
|
currentDist = (inst->mPosition - pos).len();
|
|
if ( currentDist < closestDistance )
|
|
{
|
|
closestIndex = i;
|
|
closestDistance = currentDist;
|
|
worldInstSphere.center = inst->mPosition;
|
|
worldInstSphere.radius = inst->mSize;
|
|
}
|
|
}
|
|
|
|
if ( (!collectedInsts.empty() &&
|
|
collectedInsts[closestIndex] &&
|
|
closestDistance < 1.0f) ||
|
|
worldInstSphere.isContained( pos ) )
|
|
return collectedInsts[closestIndex];
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
DecalInstance* DecalManager::raycast( const Point3F &start, const Point3F &end, bool savedDecalsOnly )
|
|
{
|
|
if ( !mData )
|
|
return NULL;
|
|
|
|
const Vector<DecalSphere*> &grid = mData->getSphereList();
|
|
|
|
DecalInstance *inst = NULL;
|
|
SphereF worldSphere( Point3F( 0, 0, 0 ), 1.0f );
|
|
|
|
Vector<DecalInstance*> hitDecals;
|
|
|
|
for ( U32 i = 0; i < grid.size(); i++ )
|
|
{
|
|
DecalSphere *decalSphere = grid[i];
|
|
if ( !decalSphere->mWorldSphere.intersectsRay( start, end ) )
|
|
continue;
|
|
|
|
const Vector<DecalInstance*> &items = decalSphere->mItems;
|
|
for ( U32 n = 0; n < items.size(); n++ )
|
|
{
|
|
inst = items[n];
|
|
if ( !inst )
|
|
continue;
|
|
|
|
if ( savedDecalsOnly && !(inst->mFlags & SaveDecal) )
|
|
continue;
|
|
|
|
worldSphere.center = inst->mPosition;
|
|
worldSphere.radius = inst->mSize;
|
|
|
|
if ( !worldSphere.intersectsRay( start, end ) )
|
|
continue;
|
|
|
|
RayInfo ri;
|
|
bool containsPoint = false;
|
|
if ( gServerContainer.castRayRendered( start, end, STATIC_COLLISION_TYPEMASK, &ri ) )
|
|
{
|
|
Point2F poly[4];
|
|
poly[0].set( inst->mPosition.x - (inst->mSize / 2), inst->mPosition.y + (inst->mSize / 2));
|
|
poly[1].set( inst->mPosition.x - (inst->mSize / 2), inst->mPosition.y - (inst->mSize / 2));
|
|
poly[2].set( inst->mPosition.x + (inst->mSize / 2), inst->mPosition.y - (inst->mSize / 2));
|
|
poly[3].set( inst->mPosition.x + (inst->mSize / 2), inst->mPosition.y + (inst->mSize / 2));
|
|
|
|
if ( MathUtils::pointInPolygon( poly, 4, Point2F(ri.point.x, ri.point.y) ) )
|
|
containsPoint = true;
|
|
}
|
|
|
|
if( !containsPoint )
|
|
continue;
|
|
|
|
hitDecals.push_back( inst );
|
|
}
|
|
}
|
|
|
|
if ( hitDecals.empty() )
|
|
return NULL;
|
|
|
|
gSortPoint = start;
|
|
dQsort( hitDecals.address(), hitDecals.size(), sizeof(DecalInstance*), cmpDecalDistance );
|
|
return hitDecals[0];
|
|
}
|
|
|
|
U32 DecalManager::_generateConvexHull( const Vector<Point3F> &points, Vector<Point3F> *outPoints )
|
|
{
|
|
PROFILE_SCOPE( DecalManager_generateConvexHull );
|
|
|
|
// chainHull_2D(): Andrew's monotone chain 2D convex hull algorithm
|
|
// Input: P[] = an array of 2D points
|
|
// presorted by increasing x- and y-coordinates
|
|
// n = the number of points in P[]
|
|
// Output: H[] = an array of the convex hull vertices (max is n)
|
|
// Return: the number of points in H[]
|
|
//int
|
|
|
|
if ( points.size() < 3 )
|
|
{
|
|
outPoints->merge( points );
|
|
return outPoints->size();
|
|
}
|
|
|
|
// Sort our input points.
|
|
dQsort( points.address(), points.size(), sizeof( Point3F ), cmpPointsXY );
|
|
|
|
U32 n = points.size();
|
|
|
|
Vector<Point3F> tmpPoints;
|
|
tmpPoints.setSize( n );
|
|
|
|
// the output array H[] will be used as the stack
|
|
S32 bot=0, top=(-1); // indices for bottom and top of the stack
|
|
S32 i; // array scan index
|
|
S32 toptmp = 0;
|
|
|
|
// Get the indices of points with min x-coord and min|max y-coord
|
|
S32 minmin = 0, minmax;
|
|
F32 xmin = points[0].x;
|
|
for ( i = 1; i < n; i++ )
|
|
if (points[i].x != xmin)
|
|
break;
|
|
|
|
minmax = i - 1;
|
|
if ( minmax == n - 1 )
|
|
{
|
|
// degenerate case: all x-coords == xmin
|
|
toptmp = top + 1;
|
|
if ( toptmp < n )
|
|
tmpPoints[++top] = points[minmin];
|
|
|
|
if ( points[minmax].y != points[minmin].y ) // a nontrivial segment
|
|
{
|
|
toptmp = top + 1;
|
|
if ( toptmp < n )
|
|
tmpPoints[++top] = points[minmax];
|
|
}
|
|
|
|
toptmp = top + 1;
|
|
if ( toptmp < n )
|
|
tmpPoints[++top] = points[minmin]; // add polygon endpoint
|
|
|
|
return top+1;
|
|
}
|
|
|
|
// Get the indices of points with max x-coord and min|max y-coord
|
|
S32 maxmin, maxmax = n-1;
|
|
F32 xmax = points[n-1].x;
|
|
|
|
for ( i = n - 2; i >= 0; i-- )
|
|
if ( points[i].x != xmax )
|
|
break;
|
|
|
|
maxmin = i + 1;
|
|
|
|
// Compute the lower hull on the stack H
|
|
toptmp = top + 1;
|
|
if ( toptmp < n )
|
|
tmpPoints[++top] = points[minmin]; // push minmin point onto stack
|
|
|
|
i = minmax;
|
|
while ( ++i <= maxmin )
|
|
{
|
|
// the lower line joins P[minmin] with P[maxmin]
|
|
if ( isLeft( points[minmin], points[maxmin], points[i]) >= 0 && i < maxmin )
|
|
continue; // ignore P[i] above or on the lower line
|
|
|
|
while (top > 0) // there are at least 2 points on the stack
|
|
{
|
|
// test if P[i] is left of the line at the stack top
|
|
if ( isLeft( tmpPoints[top-1], tmpPoints[top], points[i]) > 0)
|
|
break; // P[i] is a new hull vertex
|
|
else
|
|
top--; // pop top point off stack
|
|
}
|
|
|
|
toptmp = top + 1;
|
|
if ( toptmp < n )
|
|
tmpPoints[++top] = points[i]; // push P[i] onto stack
|
|
}
|
|
|
|
// Next, compute the upper hull on the stack H above the bottom hull
|
|
if (maxmax != maxmin) // if distinct xmax points
|
|
{
|
|
toptmp = top + 1;
|
|
if ( toptmp < n )
|
|
tmpPoints[++top] = points[maxmax]; // push maxmax point onto stack
|
|
}
|
|
|
|
bot = top; // the bottom point of the upper hull stack
|
|
i = maxmin;
|
|
while (--i >= minmax)
|
|
{
|
|
// the upper line joins P[maxmax] with P[minmax]
|
|
if ( isLeft( points[maxmax], points[minmax], points[i] ) >= 0 && i > minmax )
|
|
continue; // ignore P[i] below or on the upper line
|
|
|
|
while ( top > bot ) // at least 2 points on the upper stack
|
|
{
|
|
// test if P[i] is left of the line at the stack top
|
|
if ( isLeft( tmpPoints[top-1], tmpPoints[top], points[i] ) > 0 )
|
|
break; // P[i] is a new hull vertex
|
|
else
|
|
top--; // pop top point off stack
|
|
}
|
|
|
|
toptmp = top + 1;
|
|
if ( toptmp < n )
|
|
tmpPoints[++top] = points[i]; // push P[i] onto stack
|
|
}
|
|
|
|
if (minmax != minmin)
|
|
{
|
|
toptmp = top + 1;
|
|
if ( toptmp < n )
|
|
tmpPoints[++top] = points[minmin]; // push joining endpoint onto stack
|
|
}
|
|
|
|
outPoints->merge( tmpPoints );
|
|
|
|
return top + 1;
|
|
}
|
|
|
|
void DecalManager::_generateWindingOrder( const Point3F &cornerPoint, Vector<Point3F> *sortPoints )
|
|
{
|
|
// This block of code is used to find
|
|
// the winding order for the points in our quad.
|
|
|
|
// First, choose an arbitrary corner point.
|
|
// We'll use the "lowerRight" point.
|
|
Point3F relPoint( 0, 0, 0 );
|
|
|
|
// See comment below about radius.
|
|
//F32 radius = 0;
|
|
|
|
F32 theta = 0;
|
|
|
|
Vector<Point4F> tmpPoints;
|
|
|
|
for ( U32 i = 0; i < (*sortPoints).size(); i++ )
|
|
{
|
|
const Point3F &pnt = (*sortPoints)[i];
|
|
relPoint = cornerPoint - pnt;
|
|
|
|
// Get the radius (r^2 = x^2 + y^2).
|
|
|
|
// This is commented because for a quad
|
|
// you typically can't have the same values
|
|
// for theta, which is the caveat which would
|
|
// require sorting by the radius.
|
|
//radius = mSqrt( (relPoint.x * relPoint.x) + (relPoint.y * relPoint.y) );
|
|
|
|
// Get the theta value for the
|
|
// interval -PI, PI.
|
|
|
|
// This algorithm for determining the
|
|
// theta value is defined by
|
|
// | arctan( y / x ) if x > 0
|
|
// | arctan( y / x ) if x < 0 and y >= 0
|
|
// theta = | arctan( y / x ) if x < 0 and y < 0
|
|
// | PI / 2 if x = 0 and y > 0
|
|
// | -( PI / 2 ) if x = 0 and y < 0
|
|
if ( relPoint.x > 0.0f )
|
|
theta = mAtan2( relPoint.y, relPoint.x );
|
|
else if ( relPoint.x < 0.0f )
|
|
{
|
|
if ( relPoint.y >= 0.0f )
|
|
theta = mAtan2( relPoint.y, relPoint.x ) + M_PI_F;
|
|
else if ( relPoint.y < 0.0f )
|
|
theta = mAtan2( relPoint.y, relPoint.x ) - M_PI_F;
|
|
}
|
|
else if ( relPoint.x == 0.0f )
|
|
{
|
|
if ( relPoint.y > 0.0f )
|
|
theta = M_PI_F / 2.0f;
|
|
else if ( relPoint.y < 0.0f )
|
|
theta = -(M_PI_F / 2.0f);
|
|
}
|
|
|
|
tmpPoints.push_back( Point4F( pnt.x, pnt.y, pnt.z, theta ) );
|
|
}
|
|
|
|
dQsort( tmpPoints.address(), tmpPoints.size(), sizeof( Point4F ), cmpQuadPointTheta );
|
|
|
|
for ( U32 i = 0; i < tmpPoints.size(); i++ )
|
|
{
|
|
const Point4F &tmpPoint = tmpPoints[i];
|
|
(*sortPoints)[i].set( tmpPoint.x, tmpPoint.y, tmpPoint.z );
|
|
}
|
|
}
|
|
|
|
void DecalManager::_allocBuffers( DecalInstance *inst )
|
|
{
|
|
const S32 sizeClass = _getSizeClass( inst );
|
|
|
|
void* data;
|
|
if ( sizeClass == -1 )
|
|
data = dMalloc( sizeof( DecalVertex ) * inst->mVertCount + sizeof( U16 ) * inst->mIndxCount );
|
|
else
|
|
data = mChunkers[sizeClass]->alloc();
|
|
|
|
inst->mVerts = reinterpret_cast< DecalVertex* >( data );
|
|
data = (U8*)data + sizeof( DecalVertex ) * inst->mVertCount;
|
|
inst->mIndices = reinterpret_cast< U16* >( data );
|
|
}
|
|
|
|
void DecalManager::_freeBuffers( DecalInstance *inst )
|
|
{
|
|
if ( inst->mVerts != NULL )
|
|
{
|
|
const S32 sizeClass = _getSizeClass( inst );
|
|
|
|
if ( sizeClass == -1 )
|
|
dFree( inst->mVerts );
|
|
else
|
|
{
|
|
// Use FreeListChunker
|
|
mChunkers[sizeClass]->free( inst->mVerts );
|
|
}
|
|
|
|
inst->mVerts = NULL;
|
|
inst->mVertCount = 0;
|
|
inst->mIndices = NULL;
|
|
inst->mIndxCount = 0;
|
|
}
|
|
}
|
|
|
|
void DecalManager::_freePools()
|
|
{
|
|
while ( !mVBPool.empty() )
|
|
{
|
|
delete mVBPool.last();
|
|
mVBPool.pop_back();
|
|
}
|
|
|
|
while ( !mVBs.empty() )
|
|
{
|
|
delete mVBs.last();
|
|
mVBs.pop_back();
|
|
}
|
|
|
|
while ( !mPBPool.empty() )
|
|
{
|
|
delete mPBPool.last();
|
|
mPBPool.pop_back();
|
|
}
|
|
|
|
while ( !mPBs.empty() )
|
|
{
|
|
delete mPBs.last();
|
|
mPBs.pop_back();
|
|
}
|
|
}
|
|
|
|
S32 DecalManager::_getSizeClass( DecalInstance *inst ) const
|
|
{
|
|
U32 bytes = inst->mVertCount * sizeof( DecalVertex ) + inst->mIndxCount * sizeof ( U16 );
|
|
|
|
if ( bytes <= SIZE_CLASS_0 )
|
|
return 0;
|
|
if ( bytes <= SIZE_CLASS_1 )
|
|
return 1;
|
|
if ( bytes <= SIZE_CLASS_2 )
|
|
return 2;
|
|
|
|
// Size is outside of the largest chunker.
|
|
return -1;
|
|
}
|
|
|
|
void DecalManager::prepRenderImage( SceneRenderState* state )
|
|
{
|
|
PROFILE_SCOPE( DecalManager_RenderDecals );
|
|
|
|
if ( !smDecalsOn || !mData )
|
|
return;
|
|
|
|
// Decals only render in the diffuse pass!
|
|
// We technically could render them into reflections but prefer to save
|
|
// the performance. This would also break the DecalInstance::mLastAlpha
|
|
// optimization.
|
|
if ( !state->isDiffusePass() )
|
|
return;
|
|
|
|
PROFILE_START( DecalManager_RenderDecals_SphereTreeCull );
|
|
|
|
const Frustum& rootFrustum = state->getCameraFrustum();
|
|
|
|
// Populate vector of decal instances to be rendered with all
|
|
// decals from visible decal spheres.
|
|
|
|
SceneManager* sceneManager = state->getSceneManager();
|
|
SceneZoneSpaceManager* zoneManager = sceneManager->getZoneManager();
|
|
AssertFatal( zoneManager, "DecalManager::prepRenderImage - No zone manager!" );
|
|
const Vector<DecalSphere*> &grid = mData->getSphereList();
|
|
const bool haveOnlyOutdoorZone = ( zoneManager->getNumActiveZones() == 1 );
|
|
|
|
mDecalQueue.clear();
|
|
for ( U32 i = 0; i < grid.size(); i++ )
|
|
{
|
|
DecalSphere* decalSphere = grid[i];
|
|
const SphereF& worldSphere = decalSphere->mWorldSphere;
|
|
|
|
// See if this decal sphere can be culled.
|
|
|
|
const SceneCullingState& cullingState = state->getCullingState();
|
|
if( haveOnlyOutdoorZone )
|
|
{
|
|
U32 outdoorZone = SceneZoneSpaceManager::RootZoneId;
|
|
if( cullingState.isCulled( worldSphere, &outdoorZone, 1 ) )
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
// Update the zoning state of the sphere, if we need to.
|
|
|
|
if( decalSphere->mZones.size() == 0 )
|
|
decalSphere->updateZoning( zoneManager );
|
|
|
|
// Skip the sphere if it is not visible in any of its zones.
|
|
|
|
if( cullingState.isCulled( worldSphere, decalSphere->mZones.address(), decalSphere->mZones.size() ) )
|
|
continue;
|
|
}
|
|
|
|
// TODO: If each sphere stored its largest decal instance we
|
|
// could do an LOD step on it here and skip adding any of the
|
|
// decals in the sphere.
|
|
|
|
mDecalQueue.merge( decalSphere->mItems );
|
|
}
|
|
|
|
PROFILE_END();
|
|
|
|
PROFILE_START( DecalManager_RenderDecals_Update );
|
|
|
|
const U32 &curSimTime = Sim::getCurrentTime();
|
|
F32 pixelSize;
|
|
U32 delta, diff;
|
|
DecalInstance *dinst;
|
|
DecalData *ddata;
|
|
|
|
// Loop through DecalQueue once for preRendering work.
|
|
// 1. Update DecalInstance fade (over time)
|
|
// 2. Clip geometry if flagged to do so.
|
|
// 3. Calculate lod - if decal is far enough away it will not render.
|
|
for ( U32 i = 0; i < mDecalQueue.size(); i++ )
|
|
{
|
|
dinst = mDecalQueue[i];
|
|
ddata = dinst->mDataBlock;
|
|
|
|
// LOD calculation...
|
|
|
|
pixelSize = dinst->calcPixelSize( state->getViewport().extent.y, state->getCameraPosition(), state->getWorldToScreenScale().y );
|
|
|
|
if ( pixelSize != F32_MAX && pixelSize < ddata->fadeEndPixelSize )
|
|
{
|
|
mDecalQueue.erase_fast( i );
|
|
i--;
|
|
continue;
|
|
}
|
|
|
|
// We will try to render this decal... so do any
|
|
// final adjustments to it before rendering.
|
|
|
|
// Update fade and delete expired.
|
|
if ( !( dinst->mFlags & PermanentDecal || dinst->mFlags & CustomDecal ) )
|
|
{
|
|
delta = ( curSimTime - dinst->mCreateTime );
|
|
if ( delta > dinst->mDataBlock->lifeSpan )
|
|
{
|
|
diff = delta - dinst->mDataBlock->lifeSpan;
|
|
dinst->mVisibility = 1.0f - (F32)diff / (F32)dinst->mDataBlock->fadeTime;
|
|
|
|
if ( dinst->mVisibility <= 0.0f )
|
|
{
|
|
mDecalQueue.erase_fast( i );
|
|
removeDecal( dinst );
|
|
i--;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build clipped geometry for this decal if needed.
|
|
if ( dinst->mFlags & ClipDecal && !( dinst->mFlags & CustomDecal ) )
|
|
{
|
|
// Turn off the flag so we don't continually try to clip
|
|
// if it fails.
|
|
dinst->mFlags = dinst->mFlags & ~ClipDecal;
|
|
|
|
if ( !(dinst->mFlags & CustomDecal) && !clipDecal( dinst ) )
|
|
{
|
|
// Clipping failed to get any geometry...
|
|
|
|
// Remove it from the render queue.
|
|
mDecalQueue.erase_fast( i );
|
|
i--;
|
|
|
|
// If the decal is one placed at run-time (not the editor)
|
|
// then we should also permanently delete the decal instance.
|
|
if ( !(dinst->mFlags & SaveDecal) )
|
|
{
|
|
removeDecal( dinst );
|
|
}
|
|
|
|
// If this is a decal placed by the editor it will be
|
|
// flagged to attempt clipping again the next time it is
|
|
// modified. For now we just skip rendering it.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// If we get here and the decal still does not have any geometry
|
|
// skip rendering it. It must be an editor placed decal that failed
|
|
// to clip any geometry but has not yet been flagged to try again.
|
|
if ( !dinst->mVerts || dinst->mVertCount == 0 || dinst->mIndxCount == 0 )
|
|
{
|
|
mDecalQueue.erase_fast( i );
|
|
i--;
|
|
continue;
|
|
}
|
|
|
|
// Calculate the alpha value for this decal and apply it to the verts.
|
|
{
|
|
PROFILE_START( DecalManager_RenderDecals_Update_SetAlpha );
|
|
|
|
F32 alpha = 1.0f;
|
|
|
|
// Only necessary for decals which fade over time or distance.
|
|
if ( !( dinst->mFlags & PermanentDecal ) || dinst->mDataBlock->fadeStartPixelSize >= 0.0f )
|
|
{
|
|
if ( pixelSize < ddata->fadeStartPixelSize )
|
|
{
|
|
const F32 range = ddata->fadeStartPixelSize - ddata->fadeEndPixelSize;
|
|
alpha = 1.0f - mClampF( ( ddata->fadeStartPixelSize - pixelSize ) / range, 0.0f, 1.0f );
|
|
}
|
|
|
|
alpha *= dinst->mVisibility;
|
|
}
|
|
|
|
// If the alpha value has not changed since last render avoid
|
|
// looping through all the verts!
|
|
if ( alpha != dinst->mLastAlpha )
|
|
{
|
|
// calculate the swizzles color once, outside the loop.
|
|
GFXVertexColor color;
|
|
color.set( 255, 255, 255, (U8)(alpha * 255.0f) );
|
|
|
|
for ( U32 v = 0; v < dinst->mVertCount; v++ )
|
|
dinst->mVerts[v].color = color;
|
|
|
|
dinst->mLastAlpha = alpha;
|
|
}
|
|
|
|
PROFILE_END();
|
|
}
|
|
}
|
|
|
|
PROFILE_END();
|
|
|
|
if ( mDecalQueue.empty() )
|
|
return;
|
|
|
|
// Sort queued decals...
|
|
// 1. Editor decals - in render priority order first, creation time second, and material third.
|
|
// 2. Dynamic decals - in render priority order first and creation time second.
|
|
//
|
|
// With the constraint that decals with different render priority cannot
|
|
// be rendered together in the same draw call.
|
|
|
|
PROFILE_START( DecalManager_RenderDecals_Sort );
|
|
dQsort( mDecalQueue.address(), mDecalQueue.size(), sizeof(DecalInstance*), cmpDecalRenderOrder );
|
|
PROFILE_END();
|
|
|
|
PROFILE_SCOPE( DecalManager_RenderDecals_RenderBatch );
|
|
|
|
RenderPassManager *renderPass = state->getRenderPass();
|
|
|
|
// Base render instance we use for convenience.
|
|
// Data shared by all instances we allocate below can be copied
|
|
// from the base instance at the same time.
|
|
MeshRenderInst baseRenderInst;
|
|
baseRenderInst.clear();
|
|
|
|
MatrixF *tempMat = renderPass->allocUniqueXform( MatrixF( true ) );
|
|
MathUtils::getZBiasProjectionMatrix( gDecalBias, rootFrustum, tempMat );
|
|
baseRenderInst.projection = tempMat;
|
|
|
|
baseRenderInst.objectToWorld = &MatrixF::Identity;
|
|
baseRenderInst.worldToCamera = renderPass->allocSharedXform(RenderPassManager::View);
|
|
|
|
baseRenderInst.type = RenderPassManager::RIT_Decal;
|
|
|
|
// Make it the sort distance the max distance so that
|
|
// it renders after all the other opaque geometry in
|
|
// the prepass bin.
|
|
baseRenderInst.sortDistSq = F32_MAX;
|
|
|
|
Vector<DecalBatch> batches;
|
|
DecalBatch *currentBatch = NULL;
|
|
|
|
// Loop through DecalQueue collecting them into render batches.
|
|
for ( U32 i = 0; i < mDecalQueue.size(); i++ )
|
|
{
|
|
DecalInstance *decal = mDecalQueue[i];
|
|
DecalData *data = decal->mDataBlock;
|
|
Material *mat = data->getMaterial();
|
|
|
|
if ( currentBatch == NULL )
|
|
{
|
|
// Start a new batch, beginning with this decal.
|
|
|
|
batches.increment();
|
|
currentBatch = &batches.last();
|
|
currentBatch->startDecal = i;
|
|
currentBatch->decalCount = 1;
|
|
|
|
// Shrink and warning: preventing a potential crash.
|
|
currentBatch->iCount =
|
|
(decal->mIndxCount > smMaxIndices) ? smMaxIndices : decal->mIndxCount;
|
|
currentBatch->vCount =
|
|
(decal->mVertCount > smMaxVerts) ? smMaxVerts : decal->mVertCount;
|
|
#ifdef TORQUE_DEBUG
|
|
// we didn't mean send a spam to the console
|
|
static U32 countMsgIndx = 0;
|
|
if ( (decal->mIndxCount > smMaxIndices) && ((countMsgIndx++ % 1024) == 0) ) {
|
|
Con::warnf(
|
|
"DecalManager::prepRenderImage() - Shrinked indices of decal."
|
|
" Lost %u.", (decal->mIndxCount - smMaxIndices)
|
|
);
|
|
}
|
|
static U32 countMsgVert = 0;
|
|
if ( (decal->mVertCount > smMaxVerts) && ((countMsgVert++ % 1024) == 0) ) {
|
|
Con::warnf(
|
|
"DecalManager::prepRenderImage() - Shrinked vertices of decal."
|
|
" Lost %u.", (decal->mVertCount - smMaxVerts)
|
|
);
|
|
}
|
|
#endif
|
|
|
|
currentBatch->mat = mat;
|
|
currentBatch->matInst = decal->mDataBlock->getMaterialInstance();
|
|
currentBatch->priority = decal->getRenderPriority();
|
|
currentBatch->dynamic = !(decal->mFlags & SaveDecal);
|
|
|
|
continue;
|
|
}
|
|
|
|
if ( currentBatch->iCount + decal->mIndxCount >= smMaxIndices ||
|
|
currentBatch->vCount + decal->mVertCount >= smMaxVerts ||
|
|
currentBatch->mat != mat ||
|
|
currentBatch->priority != decal->getRenderPriority() ||
|
|
decal->mCustomTex )
|
|
{
|
|
// End batch.
|
|
|
|
currentBatch = NULL;
|
|
i--;
|
|
continue;
|
|
}
|
|
|
|
// Add on to current batch.
|
|
currentBatch->decalCount++;
|
|
currentBatch->iCount += decal->mIndxCount;
|
|
currentBatch->vCount += decal->mVertCount;
|
|
}
|
|
|
|
// Make sure our primitive and vertex buffer handle storage is
|
|
// big enough to take all batches. Doing this now avoids reallocation
|
|
// later on which would invalidate all the pointers we already had
|
|
// passed into render instances.
|
|
|
|
//mPBs.reserve( batches.size() );
|
|
//mVBs.reserve( batches.size() );
|
|
|
|
// System memory array of verts and indices so we can fill them incrementally
|
|
// and then memcpy to the graphics device buffers in one call.
|
|
static DecalVertex vertData[smMaxVerts];
|
|
static U16 indexData[smMaxIndices];
|
|
|
|
// Loop through batches allocating buffers and submitting render instances.
|
|
for ( U32 i = 0; i < batches.size(); i++ )
|
|
{
|
|
DecalBatch ¤tBatch = batches[i];
|
|
|
|
// Copy data into the system memory arrays, from all decals in this batch...
|
|
|
|
DecalVertex *vpPtr = vertData;
|
|
U16 *pbPtr = indexData;
|
|
|
|
U32 lastDecal = currentBatch.startDecal + currentBatch.decalCount;
|
|
|
|
U32 voffset = 0;
|
|
U32 ioffset = 0;
|
|
|
|
// This is an ugly hack for ProjectedShadow!
|
|
GFXTextureObject *customTex = NULL;
|
|
|
|
for ( U32 j = currentBatch.startDecal; j < lastDecal; j++ )
|
|
{
|
|
DecalInstance *dinst = mDecalQueue[j];
|
|
|
|
const U32 indxCount =
|
|
(dinst->mIndxCount > currentBatch.iCount) ?
|
|
currentBatch.iCount : dinst->mIndxCount;
|
|
for ( U32 k = 0; k < indxCount; k++ )
|
|
{
|
|
*( pbPtr + ioffset + k ) = dinst->mIndices[k] + voffset;
|
|
}
|
|
|
|
ioffset += indxCount;
|
|
|
|
const U32 vertCount =
|
|
(dinst->mVertCount > currentBatch.vCount) ?
|
|
currentBatch.vCount : dinst->mVertCount;
|
|
dMemcpy( vpPtr + voffset, dinst->mVerts, sizeof( DecalVertex ) * vertCount );
|
|
voffset += vertCount;
|
|
|
|
// Ugly hack for ProjectedShadow!
|
|
if ( (dinst->mFlags & CustomDecal) && dinst->mCustomTex != NULL )
|
|
customTex = *dinst->mCustomTex;
|
|
}
|
|
|
|
AssertFatal( ioffset == currentBatch.iCount, "bad" );
|
|
AssertFatal( voffset == currentBatch.vCount, "bad" );
|
|
|
|
// Get handles to video memory buffers we will be filling...
|
|
|
|
GFXVertexBufferHandle<DecalVertex> *vb = NULL;
|
|
|
|
if ( mVBPool.empty() )
|
|
{
|
|
// If the Pool is empty allocate a new one.
|
|
vb = new GFXVertexBufferHandle<DecalVertex>;
|
|
vb->set( GFX, smMaxVerts, GFXBufferTypeDynamic );
|
|
}
|
|
else
|
|
{
|
|
// Otherwise grab from the pool.
|
|
vb = mVBPool.last();
|
|
mVBPool.pop_back();
|
|
}
|
|
|
|
// Push into our vector of 'in use' buffers.
|
|
mVBs.push_back( vb );
|
|
|
|
// Ready to start filling.
|
|
vpPtr = vb->lock();
|
|
|
|
// Same deal as above...
|
|
GFXPrimitiveBufferHandle *pb = NULL;
|
|
if ( mPBPool.empty() )
|
|
{
|
|
pb = new GFXPrimitiveBufferHandle;
|
|
pb->set( GFX, smMaxIndices, 0, GFXBufferTypeDynamic );
|
|
}
|
|
else
|
|
{
|
|
pb = mPBPool.last();
|
|
mPBPool.pop_back();
|
|
}
|
|
mPBs.push_back( pb );
|
|
|
|
pb->lock( &pbPtr );
|
|
|
|
// Memcpy from system to video memory.
|
|
const U32 vpCount = sizeof( DecalVertex ) * currentBatch.vCount;
|
|
dMemcpy( vpPtr, vertData, vpCount );
|
|
const U32 pbCount = sizeof( U16 ) * currentBatch.iCount;
|
|
dMemcpy( pbPtr, indexData, pbCount );
|
|
|
|
pb->unlock();
|
|
vb->unlock();
|
|
|
|
// DecalManager must hold handles to these buffers so they remain valid,
|
|
// we don't actually use them elsewhere.
|
|
//mPBs.push_back( pb );
|
|
//mVBs.push_back( vb );
|
|
|
|
// Get the best lights for the current camera position
|
|
// if the materail is forward lit and we haven't got them yet.
|
|
if ( currentBatch.matInst->isForwardLit() && !baseRenderInst.lights[0] )
|
|
{
|
|
LightQuery query;
|
|
query.init( rootFrustum.getPosition(),
|
|
rootFrustum.getTransform().getForwardVector(),
|
|
rootFrustum.getFarDist() );
|
|
query.getLights( baseRenderInst.lights, 8 );
|
|
}
|
|
|
|
// Submit render inst...
|
|
MeshRenderInst *ri = renderPass->allocInst<MeshRenderInst>();
|
|
*ri = baseRenderInst;
|
|
|
|
ri->primBuff = pb;
|
|
ri->vertBuff = vb;
|
|
|
|
ri->matInst = currentBatch.matInst;
|
|
|
|
ri->prim = renderPass->allocPrim();
|
|
ri->prim->type = GFXTriangleList;
|
|
ri->prim->minIndex = 0;
|
|
ri->prim->startIndex = 0;
|
|
ri->prim->numPrimitives = currentBatch.iCount / 3;
|
|
ri->prim->startVertex = 0;
|
|
ri->prim->numVertices = currentBatch.vCount;
|
|
|
|
// Ugly hack for ProjectedShadow!
|
|
if ( customTex )
|
|
ri->miscTex = customTex;
|
|
|
|
// The decal bin will contain render instances for both decals and decalRoad's.
|
|
// Dynamic decals render last, then editor decals and roads in priority order.
|
|
// DefaultKey is sorted in descending order.
|
|
ri->defaultKey = currentBatch.dynamic ? 0xFFFFFFFF : (U32)currentBatch.priority;
|
|
ri->defaultKey2 = 1;//(U32)lastDecal->mDataBlock;
|
|
|
|
renderPass->addInst( ri );
|
|
}
|
|
|
|
#ifdef TORQUE_GATHER_METRICS
|
|
Con::setIntVariable( "$Decal::Batches", batches.size() );
|
|
Con::setIntVariable( "$Decal::Buffers", mPBs.size() + mPBPool.size() );
|
|
Con::setIntVariable( "$Decal::DecalsRendered", mDecalQueue.size() );
|
|
#endif
|
|
|
|
if( smDebugRender && gEditingMission )
|
|
{
|
|
ObjectRenderInst* ri = state->getRenderPass()->allocInst< ObjectRenderInst >();
|
|
|
|
ri->renderDelegate.bind( this, &DecalManager::_renderDecalSpheres );
|
|
ri->type = RenderPassManager::RIT_Editor;
|
|
ri->defaultKey = 0;
|
|
ri->defaultKey2 = 0;
|
|
|
|
state->getRenderPass()->addInst( ri );
|
|
}
|
|
}
|
|
|
|
void DecalManager::_renderDecalSpheres( ObjectRenderInst* ri, SceneRenderState* state, BaseMatInstance* overrideMat )
|
|
{
|
|
if( !mData )
|
|
return;
|
|
|
|
const Vector<DecalSphere*> &grid = mData->getSphereList();
|
|
|
|
GFXDrawUtil *drawUtil = GFX->getDrawUtil();
|
|
ColorI sphereColor( 0, 255, 0, 45 );
|
|
|
|
GFXStateBlockDesc desc;
|
|
desc.setBlend( true );
|
|
desc.setZReadWrite( true, false );
|
|
desc.setCullMode( GFXCullNone );
|
|
|
|
for ( U32 i = 0; i < grid.size(); i++ )
|
|
{
|
|
DecalSphere *decalSphere = grid[i];
|
|
const SphereF &worldSphere = decalSphere->mWorldSphere;
|
|
|
|
if( state->getCullingFrustum().isCulled( worldSphere ) )
|
|
continue;
|
|
|
|
drawUtil->drawSphere( desc, worldSphere.radius, worldSphere.center, sphereColor );
|
|
}
|
|
}
|
|
|
|
bool DecalManager::_createDataFile()
|
|
{
|
|
AssertFatal( !mData, "DecalManager::tried to create duplicate data file?" );
|
|
|
|
// We need to construct a default file name
|
|
char fileName[1024];
|
|
fileName[0] = 0;
|
|
|
|
// See if we know our current mission name
|
|
char missionName[1024];
|
|
dStrcpy( missionName, Con::getVariable( "$Client::MissionFile" ) );
|
|
char *dot = dStrstr((const char*)missionName, ".mis");
|
|
if(dot)
|
|
*dot = '\0';
|
|
|
|
dSprintf( fileName, sizeof(fileName), "%s.mis.decals", missionName );
|
|
|
|
mDataFileName = StringTable->insert( fileName );
|
|
|
|
// If the file doesn't exist, create an empty file.
|
|
|
|
if( !Torque::FS::IsFile( fileName ) )
|
|
{
|
|
FileStream stream;
|
|
if( stream.open( mDataFileName, Torque::FS::File::Write ) )
|
|
{
|
|
DecalDataFile dataFile;
|
|
dataFile.write( stream );
|
|
}
|
|
}
|
|
|
|
mData = ResourceManager::get().load( mDataFileName );
|
|
return (bool)mData;
|
|
}
|
|
|
|
void DecalManager::saveDecals( const UTF8* fileName )
|
|
{
|
|
if( !mData )
|
|
return;
|
|
|
|
// Create the file.
|
|
|
|
FileStream stream;
|
|
if ( !stream.open( fileName, Torque::FS::File::Write ) )
|
|
{
|
|
Con::errorf( "DecalManager::saveDecals - Could not open '%s' for writing!", fileName );
|
|
return;
|
|
}
|
|
|
|
// Write the data.
|
|
|
|
if( !mData->write( stream ) )
|
|
{
|
|
Con::errorf( "DecalManager::saveDecals - Failed to write '%s'", fileName );
|
|
return;
|
|
}
|
|
|
|
mDirty = false;
|
|
}
|
|
|
|
bool DecalManager::loadDecals( const UTF8 *fileName )
|
|
{
|
|
if( mData )
|
|
clearData();
|
|
|
|
mData = ResourceManager::get().load( fileName );
|
|
|
|
mDirty = false;
|
|
|
|
return mData != NULL;
|
|
}
|
|
|
|
void DecalManager::clearData()
|
|
{
|
|
mClearDataSignal.trigger();
|
|
|
|
// Free all geometry buffers.
|
|
|
|
if( mData )
|
|
{
|
|
const Vector< DecalSphere* > grid = mData->getSphereList();
|
|
for( U32 i = 0; i < grid.size(); ++ i )
|
|
{
|
|
DecalSphere* sphere = grid[ i ];
|
|
for( U32 n = 0; n < sphere->mItems.size(); ++ n )
|
|
_freeBuffers( sphere->mItems[ n ] );
|
|
}
|
|
}
|
|
|
|
mData = NULL;
|
|
mDecalInstanceVec.clear();
|
|
|
|
_freePools();
|
|
}
|
|
|
|
bool DecalManager::onSceneAdd()
|
|
{
|
|
if( !Parent::onSceneAdd() )
|
|
return false;
|
|
|
|
SceneZoneSpaceManager::getZoningChangedSignal().notify( this, &DecalManager::_handleZoningChangedEvent );
|
|
|
|
return true;
|
|
}
|
|
|
|
void DecalManager::onSceneRemove()
|
|
{
|
|
SceneZoneSpaceManager::getZoningChangedSignal().remove( this, &DecalManager::_handleZoningChangedEvent );
|
|
Parent::onSceneRemove();
|
|
}
|
|
|
|
void DecalManager::_handleZoningChangedEvent( SceneZoneSpaceManager* zoneManager )
|
|
{
|
|
if( zoneManager != getSceneManager()->getZoneManager() || !getDecalDataFile() )
|
|
return;
|
|
|
|
// Clear the zoning state of all DecalSpheres in the data file.
|
|
|
|
const Vector< DecalSphere* > grid = getDecalDataFile()->getSphereList();
|
|
const U32 numSpheres = grid.size();
|
|
|
|
for( U32 i = 0; i < numSpheres; ++ i )
|
|
grid[ i ]->mZones.clear();
|
|
}
|
|
|
|
DefineEngineFunction( decalManagerSave, void, ( String decalSaveFile ), ( "" ),
|
|
"Saves the decals for the active mission in the entered filename.\n"
|
|
"@param decalSaveFile Filename to save the decals to.\n"
|
|
"@tsexample\n"
|
|
"// Set the filename to save the decals in. If no filename is set, then the\n"
|
|
"// decals will default to <activeMissionName>.mis.decals\n"
|
|
"%fileName = \"./missionDecals.mis.decals\";\n"
|
|
"// Inform the decal manager to save the decals for the active mission.\n"
|
|
"decalManagerSave( %fileName );\n"
|
|
"@endtsexample\n"
|
|
"@ingroup Decals" )
|
|
{
|
|
// If not given a file name, synthesize one.
|
|
|
|
if( decalSaveFile.isEmpty() )
|
|
{
|
|
String fileName = String::ToString( "%s.decals", Con::getVariable( "$Client::MissionFile" ) );
|
|
|
|
char fullName[ 4096 ];
|
|
Platform::makeFullPathName( fileName, fullName, sizeof( fullName ) );
|
|
|
|
decalSaveFile = String( fullName );
|
|
}
|
|
|
|
// Write the data.
|
|
|
|
gDecalManager->saveDecals( decalSaveFile );
|
|
}
|
|
|
|
DefineEngineFunction( decalManagerLoad, bool, ( const char* fileName ),,
|
|
"Clears existing decals and replaces them with decals loaded from the specified file.\n"
|
|
"@param fileName Filename to load the decals from.\n"
|
|
"@return True if the decal manager was able to load the requested file, "
|
|
"false if it could not.\n"
|
|
"@tsexample\n"
|
|
"// Set the filename to load the decals from.\n"
|
|
"%fileName = \"./missionDecals.mis.decals\";\n"
|
|
"// Inform the decal manager to load the decals from the entered filename.\n"
|
|
"decalManagerLoad( %fileName );\n"
|
|
"@endtsexample\n"
|
|
"@ingroup Decals" )
|
|
{
|
|
return gDecalManager->loadDecals( fileName );
|
|
}
|
|
|
|
DefineEngineFunction( decalManagerDirty, bool, (),,
|
|
"Returns whether the decal manager has unsaved modifications.\n"
|
|
"@return True if the decal manager has unsaved modifications, false if "
|
|
"everything has been saved.\n"
|
|
"@tsexample\n"
|
|
"// Ask the decal manager if it has unsaved modifications.\n"
|
|
"%hasUnsavedModifications = decalManagerDirty();\n"
|
|
"@endtsexample\n"
|
|
"@ingroup Decals" )
|
|
{
|
|
return gDecalManager->isDirty();
|
|
}
|
|
|
|
DefineEngineFunction( decalManagerClear, void, (),,
|
|
"Removes all decals currently loaded in the decal manager.\n"
|
|
"@tsexample\n"
|
|
"// Tell the decal manager to remove all existing decals.\n"
|
|
"decalManagerClear();\n"
|
|
"@endtsexample\n"
|
|
"@ingroup Decals" )
|
|
{
|
|
gDecalManager->clearData();
|
|
}
|
|
|
|
DefineEngineFunction( decalManagerAddDecal, S32,
|
|
( Point3F position, Point3F normal, F32 rot, F32 scale, DecalData* decalData, bool isImmortal), ( false ),
|
|
"Adds a new decal to the decal manager.\n"
|
|
"@param position World position for the decal.\n"
|
|
"@param normal Decal normal vector (if the decal was a tire lying flat on a "
|
|
"surface, this is the vector pointing in the direction of the axle).\n"
|
|
"@param rot Angle (in radians) to rotate this decal around its normal vector.\n"
|
|
"@param scale Scale factor applied to the decal.\n"
|
|
"@param decalData DecalData datablock to use for the new decal.\n"
|
|
"@param isImmortal Whether or not this decal is immortal. If immortal, it "
|
|
"does not expire automatically and must be removed explicitly.\n"
|
|
"@return Returns the ID of the new Decal object or -1 on failure.\n"
|
|
"@tsexample\n"
|
|
"// Specify the decal position\n"
|
|
"%position = \"1.0 1.0 1.0\";\n\n"
|
|
"// Specify the up vector\n"
|
|
"%normal = \"0.0 0.0 1.0\";\n\n"
|
|
"// Add the new decal.\n"
|
|
"%decalObj = decalManagerAddDecal( %position, %normal, 0.5, 0.35, ScorchBigDecal, false );\n"
|
|
"@endtsexample\n"
|
|
"@ingroup Decals" )
|
|
{
|
|
if( !decalData )
|
|
{
|
|
Con::errorf( "decalManagerAddDecal - Invalid Decal DataBlock" );
|
|
return -1;
|
|
}
|
|
|
|
U8 flags = 0;
|
|
if( isImmortal )
|
|
flags |= PermanentDecal;
|
|
|
|
DecalInstance* inst = gDecalManager->addDecal( position, normal, rot, decalData, scale, -1, flags );
|
|
if( !inst )
|
|
{
|
|
Con::errorf( "decalManagerAddDecal - Unable to create decal instance." );
|
|
return -1;
|
|
}
|
|
|
|
// Add the decal to the instance vector.
|
|
inst->mId = gDecalManager->mDecalInstanceVec.size();
|
|
gDecalManager->mDecalInstanceVec.push_back( inst );
|
|
|
|
return inst->mId;
|
|
}
|
|
|
|
DefineEngineFunction( decalManagerRemoveDecal, bool, ( S32 decalID ),,
|
|
"Remove specified decal from the scene.\n"
|
|
"@param decalID ID of the decal to remove.\n"
|
|
"@return Returns true if successful, false if decal ID not found.\n"
|
|
"@tsexample\n"
|
|
"// Specify a decal ID to be removed\n"
|
|
"%decalID = 1;\n\n"
|
|
"// Tell the decal manager to remove the specified decal ID.\n"
|
|
"decalManagerRemoveDecal( %decalId )\n"
|
|
"@endtsexample\n"
|
|
"@ingroup Decals" )
|
|
{
|
|
DecalInstance *inst = gDecalManager->getDecal( decalID );
|
|
if( !inst )
|
|
return false;
|
|
|
|
gDecalManager->removeDecal(inst);
|
|
return true;
|
|
}
|
|
|
|
DefineEngineFunction( decalManagerEditDecal, bool, ( S32 decalID, Point3F pos, Point3F normal, F32 rotAroundNormal, F32 decalScale ),,
|
|
"Edit specified decal of the decal manager.\n"
|
|
"@param decalID ID of the decal to edit.\n"
|
|
"@param pos World position for the decal.\n"
|
|
"@param normal Decal normal vector (if the decal was a tire lying flat on a "
|
|
"surface, this is the vector pointing in the direction of the axle).\n"
|
|
"@param rotAroundNormal Angle (in radians) to rotate this decal around its normal vector.\n"
|
|
"@param decalScale Scale factor applied to the decal.\n"
|
|
"@return Returns true if successful, false if decalID not found.\n"
|
|
"" )
|
|
{
|
|
DecalInstance *decalInstance = gDecalManager->getDecal( decalID );
|
|
if( !decalInstance )
|
|
return false;
|
|
|
|
//Internally we need Point3F tangent instead of the user friendly F32 rotAroundNormal
|
|
MatrixF mat( true );
|
|
MathUtils::getMatrixFromUpVector( normal, &mat );
|
|
|
|
AngAxisF rot( normal, rotAroundNormal );
|
|
MatrixF rotmat;
|
|
rot.setMatrix( &rotmat );
|
|
mat.mul( rotmat );
|
|
|
|
Point3F tangent;
|
|
mat.getColumn( 1, &tangent );
|
|
|
|
//if everything is unchanged just do nothing and return "everything is ok"
|
|
if ( pos.equal(decalInstance->mPosition) &&
|
|
normal.equal(decalInstance->mNormal) &&
|
|
tangent.equal(decalInstance->mTangent) &&
|
|
mFabs( decalInstance->mSize - (decalInstance->mDataBlock->size * decalScale) ) < POINT_EPSILON )
|
|
return true;
|
|
|
|
decalInstance->mPosition = pos;
|
|
decalInstance->mNormal = normal;
|
|
decalInstance->mTangent = tangent;
|
|
decalInstance->mSize = decalInstance->mDataBlock->size * decalScale;
|
|
|
|
gDecalManager->clipDecal( decalInstance, NULL, NULL);
|
|
|
|
gDecalManager->notifyDecalModified( decalInstance );
|
|
return true;
|
|
}
|