Torque3D/Engine/source/environment/decalRoad.cpp

1740 lines
47 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 "environment/decalRoad.h"
#include "console/consoleTypes.h"
#include "console/engineAPI.h"
#include "util/catmullRom.h"
#include "math/util/quadTransforms.h"
#include "scene/sceneRenderState.h"
#include "scene/sceneManager.h"
#include "core/stream/bitStream.h"
#include "gfx/gfxDrawUtil.h"
#include "gfx/gfxTransformSaver.h"
#include "math/mathIO.h"
#include "math/mathUtils.h"
#include "terrain/terrData.h"
#include "materials/materialDefinition.h"
#include "materials/materialManager.h"
#include "materials/baseMatInstance.h"
#include "environment/nodeListManager.h"
#include "lighting/lightQuery.h"
extern F32 gDecalBias;
extern bool gEditingMission;
//-----------------------------------------------------------------------------
// DecalRoadNodeList Struct
//-----------------------------------------------------------------------------
struct DecalRoadNodeList : public NodeListManager::NodeList
{
Vector<Point3F> mPositions;
Vector<F32> mWidths;
DecalRoadNodeList() { }
virtual ~DecalRoadNodeList() { }
};
//-----------------------------------------------------------------------------
// DecalRoadNodeEvent Class
//-----------------------------------------------------------------------------
class DecalRoadNodeEvent : public NodeListEvent
{
typedef NodeListEvent Parent;
public:
Vector<Point3F> mPositions;
Vector<F32> mWidths;
public:
DecalRoadNodeEvent() { mNodeList = NULL; }
virtual ~DecalRoadNodeEvent() { }
virtual void pack(NetConnection*, BitStream*);
virtual void unpack(NetConnection*, BitStream*);
virtual void copyIntoList(NodeListManager::NodeList* copyInto);
virtual void padListToSize();
DECLARE_CONOBJECT(DecalRoadNodeEvent);
};
void DecalRoadNodeEvent::pack(NetConnection* conn, BitStream* stream)
{
Parent::pack( conn, stream );
stream->writeInt( mPositions.size(), 16 );
for (U32 i=0; i<mPositions.size(); ++i)
{
mathWrite( *stream, mPositions[i] );
stream->write( mWidths[i] );
}
}
void DecalRoadNodeEvent::unpack(NetConnection* conn, BitStream* stream)
{
mNodeList = new DecalRoadNodeList();
Parent::unpack( conn, stream );
U32 count = stream->readInt( 16 );
Point3F pos;
F32 width;
DecalRoadNodeList* list = static_cast<DecalRoadNodeList*>(mNodeList);
for (U32 i=0; i<count; ++i)
{
mathRead( *stream, &pos );
stream->read( &width );
list->mPositions.push_back( pos );
list->mWidths.push_back( width );
}
list->mTotalValidNodes = count;
// Do we have a complete list?
if (list->mPositions.size() >= mTotalNodes)
list->mListComplete = true;
}
void DecalRoadNodeEvent::copyIntoList(NodeListManager::NodeList* copyInto)
{
DecalRoadNodeList* prevList = static_cast<DecalRoadNodeList*>(copyInto);
DecalRoadNodeList* list = static_cast<DecalRoadNodeList*>(mNodeList);
// Merge our list with the old list.
for (U32 i=mLocalListStart, index=0; i<mLocalListStart+list->mPositions.size(); ++i, ++index)
{
prevList->mPositions[i] = list->mPositions[index];
prevList->mWidths[i] = list->mWidths[index];
}
}
void DecalRoadNodeEvent::padListToSize()
{
DecalRoadNodeList* list = static_cast<DecalRoadNodeList*>(mNodeList);
U32 totalValidNodes = list->mTotalValidNodes;
// Pad our list front?
if (mLocalListStart)
{
DecalRoadNodeList* newlist = new DecalRoadNodeList();
newlist->mPositions.increment(mLocalListStart);
newlist->mWidths.increment(mLocalListStart);
newlist->mPositions.merge(list->mPositions);
newlist->mWidths.merge(list->mWidths);
delete list;
mNodeList = list = newlist;
}
// Pad our list end?
if (list->mPositions.size() < mTotalNodes)
{
U32 delta = mTotalNodes - list->mPositions.size();
list->mPositions.increment(delta);
list->mWidths.increment(delta);
}
list->mTotalValidNodes = totalValidNodes;
}
IMPLEMENT_CO_NETEVENT_V1(DecalRoadNodeEvent);
ConsoleDocClass( DecalRoadNodeEvent,
"@brief Sends messages to the Decal Road Editor\n\n"
"Editor use only.\n\n"
"@internal"
);
//-----------------------------------------------------------------------------
// DecalRoadNodeListNotify Class
//-----------------------------------------------------------------------------
class DecalRoadNodeListNotify : public NodeListNotify
{
typedef NodeListNotify Parent;
protected:
SimObjectPtr<DecalRoad> mRoad;
public:
DecalRoadNodeListNotify( DecalRoad* road, U32 listId ) { mRoad = road; mListId = listId; }
virtual ~DecalRoadNodeListNotify() { mRoad = NULL; }
virtual void sendNotification( NodeListManager::NodeList* list );
};
void DecalRoadNodeListNotify::sendNotification( NodeListManager::NodeList* list )
{
if (mRoad.isValid())
{
// Build the road's nodes
DecalRoadNodeList* roadList = dynamic_cast<DecalRoadNodeList*>( list );
if (roadList)
mRoad->buildNodesFromList( roadList );
}
}
//-----------------------------------------------------------------------------
// DecalRoadUpdateEvent Class
//-----------------------------------------------------------------------------
void DecalRoadUpdateEvent::process( SimObject *object )
{
DecalRoad *road = dynamic_cast<DecalRoad*>( object );
AssertFatal( road, "DecalRoadRegenEvent::process - wasn't a DecalRoad" );
// Inform clients to perform the update.
road->setMaskBits( mMask );
if ( !road->isProperlyAdded() )
return;
// Perform the server side update.
if ( mMask & DecalRoad::TerrainChangedMask )
{
road->_generateEdges();
}
if ( mMask & DecalRoad::GenEdgesMask )
{
// Server has already done this.
//road->_generateEdges();
}
if ( mMask & DecalRoad::ReClipMask )
{
// Server does not need to capture verts.
road->_captureVerts();
}
}
//------------------------------------------------------------------------------
// Class: DecalRoad
//------------------------------------------------------------------------------
ConsoleDocClass( DecalRoad,
"@brief A strip shaped decal defined by spine nodes which clips against Terrain objects.\n\n"
"DecalRoad is for representing a road or path ( or other inventive things ) across "
"a TerrainBlock. It renders as a decal and is therefore only for features that do "
"not need geometric depth.\n\n"
"The Material assigned to DecalRoad should tile vertically.\n\n"
"@ingroup Terrain"
);
// Init Statics
// Static ConsoleVars for toggling debug rendering
bool DecalRoad::smEditorOpen = false;
bool DecalRoad::smWireframe = true;
bool DecalRoad::smShowBatches = false;
bool DecalRoad::smDiscardAll = false;
bool DecalRoad::smShowSpline = true;
bool DecalRoad::smShowRoad = true;
S32 DecalRoad::smUpdateDelay = 500;
SimObjectPtr<SimSet> DecalRoad::smServerDecalRoadSet = NULL;
// Constructors
DecalRoad::DecalRoad()
: mBreakAngle( 3.0f ),
mSegmentsPerBatch( 10 ),
mTextureLength( 5.0f ),
mRenderPriority( 10 ),
mLoadRenderData( true ),
mTriangleCount(0),
mVertCount(0),
mUpdateEventId( -1 ),
mLastEvent(NULL),
mTerrainUpdateRect( Box3F::Invalid )
{
// Setup NetObject.
mTypeMask |= StaticObjectType | StaticShapeObjectType;
mNetFlags.set(Ghostable);
INIT_ASSET(Material);
mMaterialInst = nullptr;
}
DecalRoad::~DecalRoad()
{
}
IMPLEMENT_CO_NETOBJECT_V1(DecalRoad);
// ConsoleObject
void DecalRoad::initPersistFields()
{
addGroup( "DecalRoad" );
INITPERSISTFIELD_MATERIALASSET(Material, DecalRoad, "Material used for rendering.");
addProtectedField( "textureLength", TypeF32, Offset( mTextureLength, DecalRoad ), &DecalRoad::ptSetTextureLength, &defaultProtectedGetFn,
"The length in meters of textures mapped to the DecalRoad" );
addProtectedField( "breakAngle", TypeF32, Offset( mBreakAngle, DecalRoad ), &DecalRoad::ptSetBreakAngle, &defaultProtectedGetFn,
"Angle in degrees - DecalRoad will subdivided the spline if its curve is greater than this threshold." );
addField( "renderPriority", TypeS32, Offset( mRenderPriority, DecalRoad ),
"DecalRoad(s) are rendered in descending renderPriority order." );
endGroup( "DecalRoad" );
addGroup( "Internal" );
addProtectedField( "node", TypeString, NULL, &addNodeFromField, &emptyStringProtectedGetFn,
"Do not modify, for internal use." );
endGroup( "Internal" );
Parent::initPersistFields();
}
void DecalRoad::consoleInit()
{
Parent::consoleInit();
// Vars for debug rendering while the RoadEditor is open, only used if smEditorOpen is true.
Con::addVariable( "$DecalRoad::EditorOpen", TypeBool, &DecalRoad::smEditorOpen, "For use by the Decal Editor.\n\n"
"@ingroup Editors\n" );
Con::addVariable( "$DecalRoad::wireframe", TypeBool, &DecalRoad::smWireframe, "For use by the Decal Editor.\n\n"
"@ingroup Editors\n" );
Con::addVariable( "$DecalRoad::showBatches", TypeBool, &DecalRoad::smShowBatches, "For use by the Decal Editor.\n\n"
"@ingroup Editors\n" );
Con::addVariable( "$DecalRoad::discardAll", TypeBool, &DecalRoad::smDiscardAll, "For use by the Decal Editor.\n\n"
"@ingroup Editors\n");
Con::addVariable( "$DecalRoad::showSpline", TypeBool, &DecalRoad::smShowSpline, "For use by the Decal Editor.\n\n"
"@ingroup Editors\n" );
Con::addVariable( "$DecalRoad::showRoad", TypeBool, &DecalRoad::smShowRoad, "For use by the Decal Editor.\n\n"
"@ingroup Editors\n" );
Con::addVariable( "$DecalRoad::updateDelay", TypeS32, &DecalRoad::smUpdateDelay, "For use by the Decal Editor.\n\n"
"@ingroup Editors\n" );
}
// SimObject
bool DecalRoad::onAdd()
{
if ( !Parent::onAdd() )
return false;
// DecalRoad is at position zero when created,
// it sets its own position to the first node inside
// _generateEdges but until it has at least one node
// it will be at 0,0,0.
MatrixF mat(true);
Parent::setTransform( mat );
// The client side calculates bounds based on clipped geometry. It would
// be wasteful for the server to do this so the server uses global bounds.
if ( isServerObject() )
{
setGlobalBounds();
resetWorldBox();
}
// Set the Render Transform.
setRenderTransform(mObjToWorld);
// Add to Scene.
addToScene();
if ( isServerObject() )
getServerSet()->addObject( this );
//
TerrainBlock::smUpdateSignal.notify( this, &DecalRoad::_onTerrainChanged );
//
if ( isClientObject() )
_initMaterial();
_generateEdges();
_captureVerts();
return true;
}
void DecalRoad::onRemove()
{
SAFE_DELETE( mMaterialInst );
TerrainBlock::smUpdateSignal.remove( this, &DecalRoad::_onTerrainChanged );
removeFromScene();
Parent::onRemove();
}
void DecalRoad::inspectPostApply()
{
Parent::inspectPostApply();
setMaskBits( DecalRoadMask );
}
void DecalRoad::onStaticModified( const char* slotName, const char*newValue )
{
Parent::onStaticModified( slotName, newValue );
/*
if ( isProperlyAdded() &&
dStricmp( slotName, "material" ) == 0 )
{
setMaskBits( DecalRoadMask );
}
*/
if ( dStricmp( slotName, "renderPriority" ) == 0 )
{
mRenderPriority = getMax( dAtoi(newValue), (S32)1 );
}
}
SimSet* DecalRoad::getServerSet()
{
if ( !smServerDecalRoadSet )
{
smServerDecalRoadSet = new SimSet();
smServerDecalRoadSet->registerObject( "ServerDecalRoadSet" );
Sim::getRootGroup()->addObject( smServerDecalRoadSet );
}
return smServerDecalRoadSet;
}
void DecalRoad::writeFields( Stream &stream, U32 tabStop )
{
Parent::writeFields( stream, tabStop );
// Now write all nodes
stream.write(2, "\r\n");
for ( U32 i = 0; i < mNodes.size(); i++ )
{
const RoadNode &node = mNodes[i];
stream.writeTabs(tabStop);
char buffer[1024];
dMemset( buffer, 0, 1024 );
dSprintf( buffer, 1024, "Node = \"%f %f %f %f\";", node.point.x, node.point.y, node.point.z, node.width );
stream.writeLine( (const U8*)buffer );
}
}
bool DecalRoad::writeField( StringTableEntry fieldname, const char *value )
{
if ( fieldname == StringTable->insert("node") )
return false;
return Parent::writeField( fieldname, value );
}
void DecalRoad::onEditorEnable()
{
}
void DecalRoad::onEditorDisable()
{
}
// NetObject
U32 DecalRoad::packUpdate(NetConnection * con, U32 mask, BitStream * stream)
{
// Pack Parent.
U32 retMask = Parent::packUpdate(con, mask, stream);
if ( stream->writeFlag( mask & DecalRoadMask ) )
{
// Write Texture Name.
PACK_ASSET(con, Material);
stream->write( mBreakAngle );
stream->write( mSegmentsPerBatch );
stream->write( mTextureLength );
stream->write( mRenderPriority );
}
if ( stream->writeFlag( mask & NodeMask ) )
{
//stream->writeInt( mNodes.size(), 16 );
//for ( U32 i = 0; i < mNodes.size(); i++ )
//{
// mathWrite( *stream, mNodes[i].point );
// stream->write( mNodes[i].width );
//}
const U32 nodeByteSize = 16; // Based on sending all of a node's parameters
// Test if we can fit all of our nodes within the current stream.
// We make sure we leave 100 bytes still free in the stream for whatever
// may follow us.
S32 allowedBytes = stream->getWriteByteSize() - 100;
if ( stream->writeFlag( (nodeByteSize * mNodes.size()) < allowedBytes ) )
{
// All nodes should fit, so send them out now.
stream->writeInt( mNodes.size(), 16 );
for ( U32 i = 0; i < mNodes.size(); i++ )
{
mathWrite( *stream, mNodes[i].point );
stream->write( mNodes[i].width );
}
}
else
{
// There isn't enough space left in the stream for all of the
// nodes. Batch them up into NetEvents.
U32 id = gServerNodeListManager->nextListId();
U32 count = 0;
U32 index = 0;
while (count < mNodes.size())
{
count += NodeListManager::smMaximumNodesPerEvent;
if (count > mNodes.size())
{
count = mNodes.size();
}
DecalRoadNodeEvent* event = new DecalRoadNodeEvent();
event->mId = id;
event->mTotalNodes = mNodes.size();
event->mLocalListStart = index;
for (; index<count; ++index)
{
event->mPositions.push_back( mNodes[index].point );
event->mWidths.push_back( mNodes[index].width );
}
con->postNetEvent( event );
}
stream->write( id );
}
}
stream->writeFlag( mask & GenEdgesMask );
stream->writeFlag( mask & ReClipMask );
stream->writeFlag( mask & TerrainChangedMask );
// Were done ...
return retMask;
}
void DecalRoad::unpackUpdate( NetConnection *con, BitStream *stream )
{
// Unpack Parent.
Parent::unpackUpdate( con, stream );
// DecalRoadMask
if ( stream->readFlag() )
{
UNPACK_ASSET(con, Material);
if (isProperlyAdded())
_initMaterial();
stream->read( &mBreakAngle );
stream->read( &mSegmentsPerBatch );
stream->read( &mTextureLength );
stream->read( &mRenderPriority );
}
// NodeMask
if ( stream->readFlag() )
{
//U32 count = stream->readInt( 16 );
//mNodes.clear();
//Point3F pos;
//F32 width;
//for ( U32 i = 0; i < count; i++ )
//{
// mathRead( *stream, &pos );
// stream->read( &width );
// _addNode( pos, width );
//}
if (stream->readFlag())
{
// Nodes have been passed in this update
U32 count = stream->readInt( 16 );
mNodes.clear();
Point3F pos;
F32 width;
for ( U32 i = 0; i < count; i++ )
{
mathRead( *stream, &pos );
stream->read( &width );
_addNode( pos, width );
}
}
else
{
// Nodes will arrive as events
U32 id;
stream->read( &id );
// Check if the road's nodes made it here before we did.
NodeListManager::NodeList* list = NULL;
if ( gClientNodeListManager->findListById( id, &list, true) )
{
// Work with the completed list
DecalRoadNodeList* roadList = dynamic_cast<DecalRoadNodeList*>( list );
if (roadList)
buildNodesFromList( roadList );
delete list;
}
else
{
// Nodes have not yet arrived, so register our interest in the list
DecalRoadNodeListNotify* notify = new DecalRoadNodeListNotify( this, id );
gClientNodeListManager->registerNotification( notify );
}
}
}
// GenEdgesMask
if ( stream->readFlag() && isProperlyAdded() )
_generateEdges();
// ReClipMask
if ( stream->readFlag() && isProperlyAdded() )
_captureVerts();
// TerrainChangedMask
if ( stream->readFlag() )
{
if ( isProperlyAdded() )
{
if ( mTerrainUpdateRect.isOverlapped( getWorldBox() ) )
{
_generateEdges();
_captureVerts();
// Clear out the mTerrainUpdateRect since we have updated its
// region and we now need to store future terrain changes
// in it.
mTerrainUpdateRect = Box3F::Invalid;
}
}
}
}
void DecalRoad::prepRenderImage( SceneRenderState* state )
{
PROFILE_SCOPE( DecalRoad_prepRenderImage );
if ( mNodes.size() <= 1 ||
mBatches.size() == 0 ||
!mMaterialInst ||
state->isShadowPass() )
return;
// If we don't have a material instance after the override then
// we can skip rendering all together.
BaseMatInstance *matInst = state->getOverrideMaterial(mMaterialInst);
if ( !matInst )
return;
RenderPassManager *renderPass = state->getRenderPass();
// Debug RenderInstance
// Only when editor is open.
if ( smEditorOpen )
{
ObjectRenderInst *ri = renderPass->allocInst<ObjectRenderInst>();
ri->type = RenderPassManager::RIT_Editor;
ri->renderDelegate.bind( this, &DecalRoad::_debugRender );
state->getRenderPass()->addInst( ri );
}
// Normal Road RenderInstance
// Always rendered when the editor is not open
// otherwise obey the smShowRoad flag
if ( !smShowRoad && smEditorOpen )
return;
const Frustum &frustum = state->getCameraFrustum();
MeshRenderInst coreRI;
coreRI.clear();
coreRI.objectToWorld = &MatrixF::Identity;
coreRI.worldToCamera = renderPass->allocSharedXform(RenderPassManager::View);
MatrixF *tempMat = renderPass->allocUniqueXform( MatrixF( true ) );
MathUtils::getZBiasProjectionMatrix( gDecalBias, frustum, tempMat );
coreRI.projection = tempMat;
coreRI.type = RenderPassManager::RIT_DecalRoad;
coreRI.vertBuff = &mVB;
coreRI.primBuff = &mPB;
coreRI.matInst = matInst;
// Make it the sort distance the max distance so that
// it renders after all the other opaque geometry in
// the deferred bin.
coreRI.sortDistSq = F32_MAX;
// If we need lights then set them up.
if ( matInst->isForwardLit() )
{
LightQuery query;
query.init( getWorldSphere() );
query.getLights( coreRI.lights, 8 );
}
U32 startBatchIdx = -1;
U32 endBatchIdx = 0;
for ( U32 i = 0; i < mBatches.size(); i++ )
{
const RoadBatch &batch = mBatches[i];
const bool isVisible = !frustum.isCulled( batch.bounds );
if ( isVisible )
{
// If this is the start of a set of batches.
if ( startBatchIdx == -1 )
endBatchIdx = startBatchIdx = i;
// Else we're extending the end batch index.
else
++endBatchIdx;
// If this isn't the last batch then continue.
if ( i < mBatches.size()-1 )
continue;
}
// We we still don't have a start batch, so skip.
if ( startBatchIdx == -1 )
continue;
// Render this set of batches.
const RoadBatch &startBatch = mBatches[startBatchIdx];
const RoadBatch &endBatch = mBatches[endBatchIdx];
U32 startVert = startBatch.startVert;
U32 startIdx = startBatch.startIndex;
U32 vertCount = endBatch.endVert - startVert;
U32 idxCount = ( endBatch.endIndex - startIdx ) + 1;
U32 triangleCount = idxCount / 3;
AssertFatal( startVert + vertCount <= mVertCount, "DecalRoad, bad draw call!" );
AssertFatal( startIdx + triangleCount < mTriangleCount * 3, "DecalRoad, bad draw call!" );
MeshRenderInst *ri = renderPass->allocInst<MeshRenderInst>();
*ri = coreRI;
ri->prim = renderPass->allocPrim();
ri->prim->type = GFXTriangleList;
ri->prim->minIndex = 0;
ri->prim->startIndex = startIdx;
ri->prim->numPrimitives = triangleCount;
ri->prim->startVertex = 0;
ri->prim->numVertices = endBatch.endVert + 1;
// For sorting we first sort by render priority
// and then by objectId.
//
// Since a road can submit more than one render instance, we want all
// draw calls for a single road to occur consecutively, since they
// could use the same vertex buffer.
ri->defaultKey = mRenderPriority << 0 | mId << 16;
ri->defaultKey2 = 0;
renderPass->addInst( ri );
// Reset the batching.
startBatchIdx = -1;
}
}
void DecalRoad::setTransform( const MatrixF &mat )
{
// We ignore transform requests from the editor
// right now.
}
void DecalRoad::setScale( const VectorF &scale )
{
// We ignore scale requests from the editor
// right now.
}
// DecalRoad Public Methods
bool DecalRoad::getClosestNode( const Point3F &pos, U32 &idx )
{
F32 closestDist = F32_MAX;
for ( U32 i = 0; i < mNodes.size(); i++ )
{
F32 dist = ( mNodes[i].point - pos ).len();
if ( dist < closestDist )
{
closestDist = dist;
idx = i;
}
}
return closestDist != F32_MAX;
}
bool DecalRoad::containsPoint( const Point3F &worldPos, U32 *nodeIdx ) const
{
// This is just for making selections in the editor, we use the
// client-side road because it has the proper edge's.
if ( isServerObject() && getClientObject() )
return ((DecalRoad*)getClientObject())->containsPoint( worldPos, nodeIdx );
// If point isn't in the world box,
// it's definitely not in the road.
//if ( !getWorldBox().isContained( worldPos ) )
// return false;
if ( mEdges.size() < 2 )
return false;
Point2F testPt( worldPos.x,
worldPos.y );
Point2F poly[4];
// Look through all edges, does the polygon
// formed from adjacent edge's contain the worldPos?
for ( U32 i = 0; i < mEdges.size() - 1; i++ )
{
const RoadEdge &edge0 = mEdges[i];
const RoadEdge &edge1 = mEdges[i+1];
poly[0].set( edge0.p0.x, edge0.p0.y );
poly[1].set( edge0.p2.x, edge0.p2.y );
poly[2].set( edge1.p2.x, edge1.p2.y );
poly[3].set( edge1.p0.x, edge1.p0.y );
if ( MathUtils::pointInPolygon( poly, 4, testPt ) )
{
if ( nodeIdx )
*nodeIdx = edge0.parentNodeIdx;
return true;
}
}
return false;
}
bool DecalRoad::castray( const Point3F &start, const Point3F &end ) const
{
// We just cast against the object box for the editor.
return mWorldBox.collideLine( start, end );
}
Point3F DecalRoad::getNodePosition( U32 idx )
{
if ( mNodes.size() - 1 < idx )
return Point3F();
return mNodes[idx].point;
}
void DecalRoad::setNodePosition( U32 idx, const Point3F &pos )
{
if ( mNodes.size() - 1 < idx )
return;
mNodes[idx].point = pos;
_generateEdges();
scheduleUpdate( GenEdgesMask | ReClipMask | NodeMask );
}
U32 DecalRoad::addNode( const Point3F &pos, F32 width )
{
U32 idx = _addNode( pos, width );
_generateEdges();
scheduleUpdate( GenEdgesMask | ReClipMask | NodeMask );
return idx;
}
U32 DecalRoad::insertNode(const Point3F &pos, const F32 &width, const U32 &idx)
{
U32 ret = _insertNode( pos, width, idx );
_generateEdges();
scheduleUpdate( GenEdgesMask | ReClipMask | NodeMask );
return ret;
}
void DecalRoad::setNodeWidth( U32 idx, F32 width )
{
if ( mNodes.size() - 1 < idx )
return;
mNodes[idx].width = width;
_generateEdges();
scheduleUpdate( GenEdgesMask | ReClipMask | NodeMask );
}
F32 DecalRoad::getNodeWidth( U32 idx )
{
if ( mNodes.size() - 1 < idx )
return -1.0f;
return mNodes[idx].width;
}
void DecalRoad::deleteNode( U32 idx )
{
if ( mNodes.size() - 1 < idx )
return;
mNodes.erase(idx);
_generateEdges();
scheduleUpdate( GenEdgesMask | ReClipMask | NodeMask );
}
void DecalRoad::buildNodesFromList( DecalRoadNodeList* list )
{
mNodes.clear();
for (U32 i=0; i<list->mPositions.size(); ++i)
{
_addNode( list->mPositions[i], list->mWidths[i] );
}
_generateEdges();
_captureVerts();
}
void DecalRoad::setTextureLength( F32 meters )
{
meters = getMax( meters, 0.1f );
if ( mTextureLength == meters )
return;
mTextureLength = meters;
_generateEdges();
scheduleUpdate( DecalRoadMask | ReClipMask );
}
void DecalRoad::setBreakAngle( F32 degrees )
{
//meters = getMax( meters, MIN_METERS_PER_SEGMENT );
//if ( mBreakAngle == meters )
// return;
mBreakAngle = degrees;
_generateEdges();
scheduleUpdate( DecalRoadMask | GenEdgesMask | ReClipMask );
}
void DecalRoad::scheduleUpdate( U32 updateMask )
{
scheduleUpdate( updateMask, smUpdateDelay, true );
}
void DecalRoad::scheduleUpdate( U32 updateMask, U32 delayMs, bool restartTimer )
{
if ( Sim::isEventPending( mUpdateEventId ) )
{
if ( !restartTimer )
{
mLastEvent->mMask |= updateMask;
return;
}
else
{
Sim::cancelEvent( mUpdateEventId );
}
}
mLastEvent = new DecalRoadUpdateEvent( updateMask, delayMs );
mUpdateEventId = Sim::postEvent( this, mLastEvent, Sim::getCurrentTime() + delayMs );
}
void DecalRoad::regenerate()
{
_generateEdges();
_captureVerts();
setMaskBits( NodeMask | GenEdgesMask | ReClipMask );
}
bool DecalRoad::addNodeFromField( void *object, const char *index, const char *data )
{
DecalRoad *pObj = static_cast<DecalRoad*>(object);
F32 x,y,z,width;
U32 result = dSscanf( data, "%f %f %f %f", &x, &y, &z, &width );
if ( result == 4 )
pObj->_addNode( Point3F(x,y,z), width );
return false;
}
// Internal Helper Methods
void DecalRoad::_initMaterial()
{
_setMaterial(getMaterial());
if (mMaterialAsset.notNull())
{
if (mMaterialInst && String(mMaterialAsset->getMaterialDefinitionName()).equal(mMaterialInst->getMaterial()->getName(), String::NoCase))
return;
SAFE_DELETE(mMaterialInst);
Material* tMat = nullptr;
if (!Sim::findObject(mMaterialAsset->getMaterialDefinitionName(), tMat))
Con::errorf("DecalRoad::_initMaterial - Material %s was not found.", mMaterialAsset->getMaterialDefinitionName());
mMaterial = tMat;
if (mMaterial)
mMaterialInst = mMaterial->createMatInstance();
else
mMaterialInst = MATMGR->createMatInstance("WarningMaterial");
if (!mMaterialInst)
Con::errorf("DecalRoad::_initMaterial - no Material called '%s'", mMaterialAsset->getMaterialDefinitionName());
}
if (!mMaterialInst)
return;
GFXStateBlockDesc desc;
desc.setZReadWrite( true, false );
mMaterialInst->addStateBlockDesc( desc );
mMaterialInst->init( MATMGR->getDefaultFeatures(), getGFXVertexFormat<GFXVertexPNTBT>() );
}
void DecalRoad::_debugRender( ObjectRenderInst *ri, SceneRenderState *state, BaseMatInstance* )
{
//if ( mStateBlock.isNull() )
// return;
GFX->enterDebugEvent( ColorI( 255, 0, 0 ), "DecalRoad_debugRender" );
GFXTransformSaver saver;
//GFX->setStateBlock( mStateBlock );
Point3F size(1,1,1);
ColorI color( 255, 0, 0, 255 );
GFXStateBlockDesc desc;
desc.setZReadWrite( true, false );
desc.setBlend( true );
desc.fillMode = GFXFillWireframe;
if ( smShowBatches )
{
for ( U32 i = 0; i < mBatches.size(); i++ )
{
const Box3F &box = mBatches[i].bounds;
GFX->getDrawUtil()->drawCube( desc, box, ColorI(255,100,100,255) );
}
}
//GFX->leaveDebugEvent();
}
void DecalRoad::_generateEdges()
{
PROFILE_SCOPE( DecalRoad_generateEdges );
//Con::warnf( "%s - generateEdges", isServerObject() ? "server" : "client" );
if ( mNodes.size() > 0 )
{
// Set our object position to the first node.
const Point3F &nodePt = mNodes.first().point;
MatrixF mat( true );
mat.setPosition( nodePt );
Parent::setTransform( mat );
// The server object has global bounds, which Parent::setTransform
// messes up so we must reset it.
if ( isServerObject() )
{
mObjBox.minExtents.set(-1e10, -1e10, -1e10);
mObjBox.maxExtents.set( 1e10, 1e10, 1e10);
}
}
if ( mNodes.size() < 2 )
return;
// Ensure nodes are above the terrain height at their xy position
for ( U32 i = 0; i < mNodes.size(); i++ )
{
_getTerrainHeight( mNodes[i].point );
}
// Now start generating edges...
U32 nodeCount = mNodes.size();
Point3F *positions = new Point3F[nodeCount];
for ( U32 i = 0; i < nodeCount; i++ )
{
const RoadNode &node = mNodes[i];
positions[i].set( node.point.x, node.point.y, node.width );
}
CatmullRom<Point3F> spline;
spline.initialize( nodeCount, positions );
delete [] positions;
mEdges.clear();
Point3F lastBreakVector(0,0,0);
RoadEdge slice;
Point3F lastBreakNode;
lastBreakNode = spline.evaluate(0.0f);
for ( U32 i = 1; i < mNodes.size(); i++ )
{
F32 t1 = spline.getTime(i);
F32 t0 = spline.getTime(i-1);
F32 segLength = spline.arcLength( t0, t1 );
U32 numSegments = mCeil( segLength / MIN_METERS_PER_SEGMENT );
numSegments = getMax( numSegments, (U32)1 );
F32 tstep = ( t1 - t0 ) / numSegments;
U32 startIdx = 0;
U32 endIdx = ( i == nodeCount - 1 ) ? numSegments + 1 : numSegments;
for ( U32 j = startIdx; j < endIdx; j++ )
{
F32 t = t0 + tstep * j;
Point3F splineNode = spline.evaluate(t);
F32 width = splineNode.z;
_getTerrainHeight( splineNode );
Point3F toNodeVec = splineNode - lastBreakNode;
toNodeVec.normalizeSafe();
if ( lastBreakVector.isZero() )
lastBreakVector = toNodeVec;
F32 angle = mRadToDeg( mAcos( mDot( toNodeVec, lastBreakVector ) ) );
if ( j == startIdx ||
( j == endIdx - 1 && i == mNodes.size() - 1 ) ||
angle > mBreakAngle )
{
// Push back a spline node
//slice.p1.set( splineNode.x, splineNode.y, 0.0f );
//_getTerrainHeight( slice.p1 );
slice.p1 = splineNode;
slice.uvec.set(0,0,1);
slice.width = width;
slice.parentNodeIdx = i-1;
mEdges.push_back( slice );
lastBreakVector = splineNode - lastBreakNode;
lastBreakVector.normalizeSafe();
lastBreakNode = splineNode;
}
}
}
/*
for ( U32 i = 1; i < nodeCount; i++ )
{
F32 t0 = spline.getTime( i-1 );
F32 t1 = spline.getTime( i );
F32 segLength = spline.arcLength( t0, t1 );
U32 numSegments = mCeil( segLength / mBreakAngle );
numSegments = getMax( numSegments, (U32)1 );
F32 tstep = ( t1 - t0 ) / numSegments;
AssertFatal( numSegments > 0, "DecalRoad::_generateEdges, got zero segments!" );
U32 startIdx = 0;
U32 endIdx = ( i == nodeCount - 1 ) ? numSegments + 1 : numSegments;
for ( U32 j = startIdx; j < endIdx; j++ )
{
F32 t = t0 + tstep * j;
Point3F val = spline.evaluate(t);
RoadEdge edge;
edge.p1.set( val.x, val.y, 0.0f );
_getTerrainHeight( val.x, val.y, edge.p1.z );
edge.uvec.set(0,0,1);
edge.width = val.z;
edge.parentNodeIdx = i-1;
mEdges.push_back( edge );
}
}
*/
//
// Calculate fvec and rvec for all edges
//
RoadEdge *edge = NULL;
RoadEdge *nextEdge = NULL;
for ( U32 i = 0; i < mEdges.size() - 1; i++ )
{
edge = &mEdges[i];
nextEdge = &mEdges[i+1];
edge->fvec = nextEdge->p1 - edge->p1;
edge->fvec.normalize();
edge->rvec = mCross( edge->fvec, edge->uvec );
edge->rvec.normalize();
}
// Must do the last edge outside the loop
RoadEdge *lastEdge = &mEdges[mEdges.size()-1];
RoadEdge *prevEdge = &mEdges[mEdges.size()-2];
lastEdge->fvec = prevEdge->fvec;
lastEdge->rvec = prevEdge->rvec;
//
// Calculate p0/p2 for all edges
//
for ( U32 i = 0; i < mEdges.size(); i++ )
{
edge = &mEdges[i];
edge->p0 = edge->p1 - edge->rvec * edge->width * 0.5f;
edge->p2 = edge->p1 + edge->rvec * edge->width * 0.5f;
_getTerrainHeight( edge->p0 );
_getTerrainHeight( edge->p2 );
}
}
void DecalRoad::_captureVerts()
{
PROFILE_SCOPE( DecalRoad_captureVerts );
//Con::warnf( "%s - captureVerts", isServerObject() ? "server" : "client" );
if ( isServerObject() )
{
//Con::errorf( "DecalRoad::_captureVerts - called on the server side!" );
return;
}
if ( mEdges.size() == 0 )
return;
//
// Construct ClippedPolyList objects for each pair
// of roadEdges.
// Use them to capture Terrain verts.
//
SphereF sphere;
RoadEdge *edge = NULL;
RoadEdge *nextEdge = NULL;
mTriangleCount = 0;
mVertCount = 0;
Vector<ClippedPolyList> clipperList;
for ( U32 i = 0; i < mEdges.size() - 1; i++ )
{
Box3F box;
edge = &mEdges[i];
nextEdge = &mEdges[i+1];
box.minExtents = edge->p1;
box.maxExtents = edge->p1;
box.extend( edge->p0 );
box.extend( edge->p2 );
box.extend( nextEdge->p0 );
box.extend( nextEdge->p1 );
box.extend( nextEdge->p2 );
box.minExtents.z -= 5.0f;
box.maxExtents.z += 5.0f;
sphere.center = ( nextEdge->p1 + edge->p1 ) * 0.5f;
sphere.radius = 100.0f; // NOTE: no idea how to calculate this
ClippedPolyList clipper;
clipper.mNormal.set(0.0f, 0.0f, 0.0f);
VectorF n;
PlaneF plane0, plane1;
// Construct Back Plane
n = edge->p2 - edge->p0;
n.normalize();
n = mCross( n, edge->uvec );
plane0.set( edge->p0, n );
clipper.mPlaneList.push_back( plane0 );
// Construct Front Plane
n = nextEdge->p2 - nextEdge->p0;
n.normalize();
n = -mCross( edge->uvec, n );
plane1.set( nextEdge->p0, -n );
//clipper.mPlaneList.push_back( plane1 );
// Test if / where the planes intersect.
bool discardLeft = false;
bool discardRight = false;
Point3F iPos;
VectorF iDir;
if ( plane0.intersect( plane1, iPos, iDir ) )
{
Point2F iPos2F( iPos.x, iPos.y );
Point2F cPos2F( edge->p1.x, edge->p1.y );
Point2F rVec2F( edge->rvec.x, edge->rvec.y );
Point2F iVec2F = iPos2F - cPos2F;
F32 iLen = iVec2F.len();
iVec2F.normalize();
if ( iLen < edge->width * 0.5f )
{
F32 dot = mDot( rVec2F, iVec2F );
// The clipping planes intersected on the right side,
// discard the right side clipping plane.
if ( dot > 0.0f )
discardRight = true;
// The clipping planes intersected on the left side,
// discard the left side clipping plane.
else
discardLeft = true;
}
}
// Left Plane
if ( !discardLeft )
{
n = ( nextEdge->p0 - edge->p0 );
n.normalize();
n = mCross( edge->uvec, n );
clipper.mPlaneList.push_back( PlaneF(edge->p0, n) );
}
else
{
nextEdge->p0 = edge->p0;
}
// Right Plane
if ( !discardRight )
{
n = ( nextEdge->p2 - edge->p2 );
n.normalize();
n = -mCross( n, edge->uvec );
clipper.mPlaneList.push_back( PlaneF(edge->p2, -n) );
}
else
{
nextEdge->p2 = edge->p2;
}
n = nextEdge->p2 - nextEdge->p0;
n.normalize();
n = -mCross( edge->uvec, n );
plane1.set( nextEdge->p0, -n );
clipper.mPlaneList.push_back( plane1 );
// We have constructed the clipping planes,
// now grab/clip the terrain geometry
getContainer()->buildPolyList( PLC_Decal, box, TerrainObjectType, &clipper );
clipper.cullUnusedVerts();
clipper.triangulate();
clipper.generateNormals();
// If we got something, add it to the ClippedPolyList Vector
if ( !clipper.isEmpty() && !( smDiscardAll && ( discardRight || discardLeft ) ) )
{
clipperList.push_back( clipper );
mVertCount += clipper.mVertexList.size();
mTriangleCount += clipper.mPolyList.size();
}
}
//
// Set the roadEdge height to be flush with terrain
// This is not really necessary but makes the debug spline rendering better.
//
for ( U32 i = 0; i < mEdges.size() - 1; i++ )
{
edge = &mEdges[i];
_getTerrainHeight( edge->p0.x, edge->p0.y, edge->p0.z );
_getTerrainHeight( edge->p2.x, edge->p2.y, edge->p2.z );
}
//
// Allocate the RoadBatch(s)
//
// If we captured no verts, then we can return here without
// allocating any RoadBatches or the Vert/Index Buffers.
// PreprenderImage will not allocate a render instance while
// mBatches.size() is zero.
U32 numClippers = clipperList.size();
if ( numClippers == 0 )
return;
mBatches.clear();
// Allocate the VertexBuffer and PrimitiveBuffer
mVB.set( GFX, mVertCount, GFXBufferTypeStatic );
mPB.set( GFX, mTriangleCount * 3, 0, GFXBufferTypeStatic );
// Lock the VertexBuffer
GFXVertexPNTBT *vertPtr = mVB.lock();
if(!vertPtr) return;
U32 vertIdx = 0;
//
// Fill the VertexBuffer and vertex data for the RoadBatches
// Loop through the ClippedPolyList Vector
//
RoadBatch *batch = NULL;
F32 texStart = 0.0f;
F32 texEnd;
for ( U32 i = 0; i < clipperList.size(); i++ )
{
ClippedPolyList *clipper = &clipperList[i];
edge = &mEdges[i];
nextEdge = &mEdges[i+1];
VectorF segFvec = nextEdge->p1 - edge->p1;
F32 segLen = segFvec.len();
segFvec.normalize();
F32 texLen = segLen / mTextureLength;
texEnd = texStart + texLen;
BiQuadToSqr quadToSquare( Point2F( edge->p0.x, edge->p0.y ),
Point2F( edge->p2.x, edge->p2.y ),
Point2F( nextEdge->p2.x, nextEdge->p2.y ),
Point2F( nextEdge->p0.x, nextEdge->p0.y ) );
//
if ( i % mSegmentsPerBatch == 0 )
{
mBatches.increment();
batch = &mBatches.last();
batch->bounds.minExtents = clipper->mVertexList[0].point;
batch->bounds.maxExtents = clipper->mVertexList[0].point;
batch->startVert = vertIdx;
}
// Loop through each ClippedPolyList
for ( U32 j = 0; j < clipper->mVertexList.size(); j++ )
{
// Add each vert to the VertexBuffer
Point3F pos = clipper->mVertexList[j].point;
vertPtr[vertIdx].point = pos;
vertPtr[vertIdx].normal = clipper->mNormalList[j];
Point2F uv = quadToSquare.transform( Point2F(pos.x,pos.y) );
vertPtr[vertIdx].texCoord.x = uv.x;
vertPtr[vertIdx].texCoord.y = -(( texEnd - texStart ) * uv.y + texStart);
vertPtr[vertIdx].tangent = mCross( segFvec, clipper->mNormalList[j] );
vertPtr[vertIdx].binormal = segFvec;
vertIdx++;
// Expand the RoadBatch bounds to contain this vertex
batch->bounds.extend( pos );
}
batch->endVert = vertIdx - 1;
texStart = texEnd;
}
// Unlock the VertexBuffer, we are done filling it.
mVB.unlock();
// Lock the PrimitiveBuffer
U16 *idxBuff;
mPB.lock(&idxBuff);
U32 curIdx = 0;
U16 vertOffset = 0;
batch = NULL;
S32 batchIdx = -1;
// Fill the PrimitiveBuffer
// Loop through each ClippedPolyList in the Vector
for ( U32 i = 0; i < clipperList.size(); i++ )
{
ClippedPolyList *clipper = &clipperList[i];
if ( i % mSegmentsPerBatch == 0 )
{
batchIdx++;
batch = &mBatches[batchIdx];
batch->startIndex = curIdx;
}
for ( U32 j = 0; j < clipper->mPolyList.size(); j++ )
{
// Write indices for each Poly
ClippedPolyList::Poly *poly = &clipper->mPolyList[j];
AssertFatal( poly->vertexCount == 3, "Got non-triangle poly!" );
idxBuff[curIdx] = clipper->mIndexList[poly->vertexStart] + vertOffset;
curIdx++;
idxBuff[curIdx] = clipper->mIndexList[poly->vertexStart + 1] + vertOffset;
curIdx++;
idxBuff[curIdx] = clipper->mIndexList[poly->vertexStart + 2] + vertOffset;
curIdx++;
}
batch->endIndex = curIdx - 1;
vertOffset += clipper->mVertexList.size();
}
// Unlock the PrimitiveBuffer, we are done filling it.
mPB.unlock();
// Generate the object/world bounds
// Is the union of all batch bounding boxes.
Box3F box;
for ( U32 i = 0; i < mBatches.size(); i++ )
{
batch = &mBatches[i];
if ( i == 0 )
box = batch->bounds;
else
box.intersect( batch->bounds );
}
mWorldBox = box;
resetObjectBox();
// Make sure we are in the correct bins given our world box.
if( getSceneManager() != NULL )
getSceneManager()->notifyObjectDirty( this );
}
U32 DecalRoad::_addNode( const Point3F &pos, F32 width )
{
mNodes.increment();
RoadNode &node = mNodes.last();
node.point = pos;
node.width = width;
return mNodes.size() - 1;
}
U32 DecalRoad::_insertNode( const Point3F &pos, const F32 &width, const U32 &idx )
{
U32 ret;
RoadNode *node;
if ( idx == U32_MAX )
{
mNodes.increment();
node = &mNodes.last();
ret = mNodes.size() - 1;
}
else
{
mNodes.insert( idx );
node = &mNodes[idx];
ret = idx;
}
node->point = pos;
//node->t = -1.0f;
//node->rot.identity();
node->width = width;
return ret;
}
bool DecalRoad::_getTerrainHeight( Point3F &pos )
{
return _getTerrainHeight( pos.x, pos.y, pos.z );
}
bool DecalRoad::_getTerrainHeight( const Point2F &pos, F32 &height )
{
return _getTerrainHeight( pos.x, pos.y, height );
}
bool DecalRoad::_getTerrainHeight( const F32 &x, const F32 &y, F32 &height )
{
Point3F startPnt( x, y, 10000.0f );
Point3F endPnt( x, y, -10000.0f );
RayInfo ri;
bool hit;
hit = getContainer()->castRay(startPnt, endPnt, TerrainObjectType, &ri);
if ( hit )
height = ri.point.z;
return hit;
}
void DecalRoad::_onTerrainChanged( U32 type, TerrainBlock* tblock, const Point2I &min, const Point2I &max )
{
// The client side object just stores the area that has changed
// and waits for the (delayed) update event from the server
// to actually perform the update.
if ( isClientObject() && tblock->isClientObject() )
{
// Convert the min and max into world space.
const F32 size = tblock->getSquareSize();
const Point3F pos = tblock->getPosition();
// TODO: I don't think this works right with tiling!
Box3F dirty( F32( min.x * size ) + pos.x, F32( min.y * size ) + pos.y, -F32_MAX,
F32( max.x * size ) + pos.x, F32( max.y * size ) + pos.y, F32_MAX );
if ( !mTerrainUpdateRect.isValidBox() )
mTerrainUpdateRect = dirty;
else
mTerrainUpdateRect.intersect( dirty );
}
// The server object only updates edges (doesn't clip to geometry)
// and schedules an update to be sent to the client.
else if ( isServerObject() && tblock->isServerObject() )
{
//_generateEdges();
scheduleUpdate( TerrainChangedMask );
}
}
// Static protected field set methods
bool DecalRoad::ptSetBreakAngle( void *object, const char *index, const char *data )
{
DecalRoad *road = static_cast<DecalRoad*>( object );
F32 val = dAtof( data );
road->setBreakAngle( val );
// we already set the field
return false;
}
bool DecalRoad::ptSetTextureLength( void *object, const char *index, const char *data )
{
DecalRoad *road = static_cast<DecalRoad*>( object );
F32 val = dAtof( data );
road->setTextureLength( val );
// we already set the field
return false;
}
// ConsoleMethods
DefineEngineMethod( DecalRoad, regenerate, void, (),,
"Intended as a helper to developers and editor scripts.\n"
"Force DecalRoad to update it's spline and reclip geometry."
)
{
object->regenerate();
}
DefineEngineMethod( DecalRoad, postApply, void, (),,
"Intended as a helper to developers and editor scripts.\n"
"Force trigger an inspectPostApply. This will transmit "
"the material and other fields ( not including nodes ) "
"to client objects."
)
{
object->inspectPostApply();
}