Torque3D/Engine/source/T3D/fx/groundCover.cpp
Daniel Buckmaster 027d5608bd Added comment.
2012-11-13 10:21:03 +11:00

1698 lines
56 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/fx/groundCover.h"
#include "core/resourceManager.h"
#include "core/stream/bitStream.h"
#include "console/consoleTypes.h"
#include "scene/sceneRenderState.h"
#include "terrain/terrData.h"
#include "renderInstance/renderPassManager.h"
#include "gfx/gfxDrawUtil.h"
#include "gfx/primBuilder.h"
#include "T3D/gameBase/gameConnection.h"
#include "gfx/gfxVertexBuffer.h"
#include "gfx/gfxStructs.h"
#include "ts/tsShapeInstance.h"
#include "lighting/lightManager.h"
#include "lighting/lightInfo.h"
#include "materials/shaderData.h"
#include "gfx/gfxTransformSaver.h"
#include "shaderGen/shaderGenVars.h"
#include "materials/matTextureTarget.h"
#include "gfx/util/screenspace.h"
#include "materials/materialDefinition.h"
#include "materials/materialManager.h"
#include "materials/sceneData.h"
#include "materials/materialFeatureTypes.h"
#include "materials/matInstance.h"
#include "renderInstance/renderPrePassMgr.h"
#include "console/engineAPI.h"
/// This is used for rendering ground cover billboards.
GFXImplementVertexFormat( GCVertex )
{
addElement( "POSITION", GFXDeclType_Float3 );
addElement( "NORMAL", GFXDeclType_Float3 );
addElement( "COLOR", GFXDeclType_Color );
addElement( "TEXCOORD", GFXDeclType_Float4, 0 );
};
GroundCoverShaderConstHandles::GroundCoverShaderConstHandles()
: mTypeRectsSC( NULL ),
mFadeSC( NULL ),
mWindDirSC( NULL ),
mGustInfoSC( NULL ),
mTurbInfoSC( NULL ),
mCamRightSC( NULL ),
mCamUpSC( NULL ),
mGroundCover( NULL )
{
}
void GroundCoverShaderConstHandles::init( GFXShader *shader )
{
mTypeRectsSC = shader->getShaderConstHandle( "$gc_typeRects" );
mFadeSC = shader->getShaderConstHandle( "$gc_fadeParams" );
mWindDirSC = shader->getShaderConstHandle( "$gc_windDir" );
mGustInfoSC = shader->getShaderConstHandle( "$gc_gustInfo" );
mTurbInfoSC = shader->getShaderConstHandle( "$gc_turbInfo" );
mCamRightSC = shader->getShaderConstHandle( "$gc_camRight" );
mCamUpSC = shader->getShaderConstHandle( "$gc_camUp" );
}
void GroundCoverShaderConstHandles::setConsts( SceneRenderState *state, const SceneData &sgData, GFXShaderConstBuffer *buffer )
{
AlignedArray<Point4F> rectData( MAX_COVERTYPES, sizeof( Point4F ), (U8*)(mGroundCover->mBillboardRects), false );
buffer->setSafe( mTypeRectsSC, rectData );
const GroundCoverShaderConstData &data = mGroundCover->getShaderConstData();
buffer->setSafe( mFadeSC, data.fadeInfo );
buffer->setSafe( mWindDirSC, mGroundCover->mWindDirection );
buffer->setSafe( mGustInfoSC, data.gustInfo );
buffer->setSafe( mTurbInfoSC, data.turbInfo );
buffer->setSafe( mCamRightSC, data.camRight );
buffer->setSafe( mCamUpSC, data.camUp );
}
/// This defines one grid cell.
class GroundCoverCell
{
protected:
friend class GroundCover;
struct Placement
{
Point3F point;
Point3F normal;
Point3F size;
F32 rotation;
U32 type;
F32 windAmplitude;
Box3F worldBox;
ColorF lmColor;
};
/// This is the x,y index for this cell.
Point2I mIndex;
/// The worldspace bounding box this cell.
Box3F mBounds;
/// The worldspace bounding box of the renderable
/// content within this cell.
Box3F mRenderBounds;
/// The instances of billboard cover elements in this cell.
Vector<Placement> mBillboards;
/// The instances of shape cover elements in this cell.
Vector<Placement> mShapes;
typedef GFXVertexBufferHandle<GCVertex> VBHandle;
typedef Vector< VBHandle > VBHandleVector;
/// The vertex buffers that hold all the
/// prepared billboards for this cell.
VBHandleVector mVBs;
/// Used to mark the cell dirty and in need
/// of a rebuild.
bool mDirty;
/// Repacks the billboards into the vertex buffer.
void _rebuildVB();
public:
GroundCoverCell() {}
~GroundCoverCell()
{
mVBs.clear();
}
const Point2I& shiftIndex( const Point2I& shift ) { return mIndex += shift; }
/// The worldspace bounding box this cell.
const Box3F& getBounds() const { return mBounds; }
/// The worldspace bounding box of the renderable
/// content within this cell.
const Box3F& getRenderBounds() const { return mRenderBounds; }
Point3F getCenter() const { return mBounds.getCenter(); }
VectorF getSize() const { return VectorF( mBounds.len_x() / 2.0f,
mBounds.len_y() / 2.0f,
mBounds.len_z() / 2.0f ); }
void renderBillboards( SceneRenderState *state, BaseMatInstance *mat, GFXPrimitiveBufferHandle *pb );
U32 renderShapes( const TSRenderState &rdata,
Frustum *culler,
TSShapeInstance** shapes );
};
void GroundCoverCell::_rebuildVB()
{
if ( mBillboards.empty() )
return;
PROFILE_SCOPE(GroundCover_RebuildVB);
// The maximum verts we can put in one vertex buffer batch.
const U32 MAX_BILLBOARDS = 0xFFFF / 4;
// How many batches will we need in total?
const U32 batches = mCeil( (F32)mBillboards.size() / (F32)MAX_BILLBOARDS );
// So... how many billboards do we need in
// each batch? We're trying to evenly divide
// the amount across all the VBs.
const U32 batchBB = mBillboards.size() / batches;
// Init the vertex buffer list to the right size. Any
// VBs already in there will remain unless we're truncating
// the list... those are freed.
mVBs.setSize( batches );
// Get the iter to the first billboard.
Vector<Placement>::const_iterator iter = mBillboards.begin();
// Prepare each batch.
U32 bb, remaining = mBillboards.size();
for ( U32 b = 0; b < batches; b++ )
{
// Grab a reference to the vb.
VBHandle &vb = mVBs[b];
// How many billboards in this batch?
bb = getMin( batchBB, remaining );
remaining -= bb;
// Ok... now how many verts is that?
const U32 verts = bb * 4;
// Create the VB hasn't been created or if its
// too small then resize it.
if ( vb.isNull() || vb->mNumVerts < verts )
{
PROFILE_START(GroundCover_CreateVB);
vb.set( GFX, verts, GFXBufferTypeStatic );
PROFILE_END();
}
// Fill this puppy!
GCVertex* vertPtr = vb.lock( 0, verts );
GFXVertexColor color;
Vector<Placement>::const_iterator last = iter + bb;
for ( ; iter != last; iter++ )
{
const Point3F &position = (*iter).point;
const Point3F &normal = (*iter).normal;
const S32 &type = (*iter).type;
const Point3F &size = (*iter).size;
const F32 &windAmplitude = (*iter).windAmplitude;
GFXVertexColor color = (ColorI)(*iter).lmColor;
U8 *col = (U8 *)const_cast<U32 *>( (const U32 *)color );
vertPtr->point = position;
vertPtr->normal = normal;
vertPtr->params.x = size.x;
vertPtr->params.y = size.y;
vertPtr->params.z = type;
vertPtr->params.w = 0;
col[3] = 0;
vertPtr->ambient = color;
++vertPtr;
vertPtr->point = position;
vertPtr->normal = normal;
vertPtr->params.x = size.x;
vertPtr->params.y = size.y;
vertPtr->params.z = type;
vertPtr->params.w = 0;
col[3] = 1;
vertPtr->ambient = color;
++vertPtr;
vertPtr->point = position;
vertPtr->normal = normal;
vertPtr->params.x = size.x;
vertPtr->params.y = size.y;
vertPtr->params.z = type;
vertPtr->params.w = windAmplitude;
col[3] = 2;
vertPtr->ambient = color;
++vertPtr;
vertPtr->point = position;
vertPtr->normal = normal;
vertPtr->params.x = size.x;
vertPtr->params.y = size.y;
vertPtr->params.z = type;
vertPtr->params.w = windAmplitude;
col[3] = 3;
vertPtr->ambient = color;
++vertPtr;
}
vb.unlock();
}
}
U32 GroundCoverCell::renderShapes( const TSRenderState &rdata,
Frustum *culler,
TSShapeInstance** shapes )
{
MatrixF worldMat;
TSShapeInstance* shape;
Point3F camVector;
F32 dist;
F32 invScale;
const SceneRenderState *state = rdata.getSceneState();
U32 totalRendered = 0;
Vector<Placement>::const_iterator iter = mShapes.begin();
for ( ; iter != mShapes.end(); iter++ )
{
// Grab a reference here once.
const Placement& inst = (*iter);
// If we were pass a culler then us it to test the shape world box.
if ( culler && culler->isCulled( inst.worldBox ) )
continue;
shape = shapes[ inst.type ];
camVector = inst.point - state->getDiffuseCameraPosition();
dist = getMax( camVector.len(), 0.01f );
worldMat.set( EulerF(0, 0, inst.rotation), inst.point );
// TSShapeInstance::render() uses the
// world matrix for the RenderInst.
worldMat.scale( inst.size );
GFX->setWorldMatrix( worldMat );
// Obey the normal screen space lod metrics. The shapes should
// be tuned to lod out quickly for ground cover.
//
// Note: The profile doesn't indicate that lod selection is
// very expensive... in fact its less than 1/10th of the cost
// of the render() call below.
PROFILE_START(GroundCover_RenderShapes_SelectDetail);
invScale = (1.0f/getMax(getMax(inst.size.x,inst.size.y),inst.size.z));
shape->setDetailFromDistance( state, dist * invScale );
PROFILE_END(); // GroundCover_RenderShapes_SelectDetail
// Note: This is the most expensive call of this loop. We
// need to rework the render call completely to optimize it.
PROFILE_START(GroundCover_RenderShapes_Render);
shape->render( rdata );
PROFILE_END(); // GroundCover_RenderShapes_Render
totalRendered++;
}
return totalRendered;
}
void GroundCoverCell::renderBillboards( SceneRenderState *state, BaseMatInstance *mat, GFXPrimitiveBufferHandle *pb )
{
if ( mDirty )
{
_rebuildVB();
mDirty = false;
}
// Do we have anything to render?
if ( mBillboards.size() == 0 || mVBs.empty() || !mat )
return;
// TODO: Maybe add support for non-facing billboards
// with random rotations and optional crosses. We could
// stick them into the buffer after the normal billboards,
// then change shader consts.
RenderPassManager *pass = state->getRenderPass();
// Draw each batch.
U32 remaining = mBillboards.size();
const U32 batches = mVBs.size();
const U32 batchBB = remaining / batches;
for ( U32 b = 0; b < batches; b++ )
{
// Grab a reference to the vb.
VBHandle &vb = mVBs[b];
// How many billboards in this batch?
U32 bb = getMin( batchBB, remaining );
remaining -= bb;
MeshRenderInst *ri = pass->allocInst<MeshRenderInst>();
ri->type = RenderPassManager::RIT_Mesh;
ri->matInst = mat;
ri->vertBuff = &vb;
ri->primBuff = pb;
ri->objectToWorld = &MatrixF::Identity;
ri->worldToCamera = pass->allocSharedXform(RenderPassManager::View);
ri->projection = pass->allocSharedXform(RenderPassManager::Projection);
ri->defaultKey = mat->getStateHint();
ri->prim = pass->allocPrim();
ri->prim->numPrimitives = bb * 2;
ri->prim->numVertices = bb * 4;
ri->prim->startIndex = 0;
ri->prim->startVertex = 0;
ri->prim->minIndex = 0;
ri->prim->type = GFXTriangleList;
// If we need lights then set them up.
if ( mat->isForwardLit() )
{
LightQuery query;
query.init( mBounds );
query.getLights( ri->lights, 8 );
}
pass->addInst( ri );
GroundCover::smStatRenderedBatches++;
GroundCover::smStatRenderedBillboards += bb;
}
GroundCover::smStatRenderedCells++;
}
U32 GroundCover::smStatRenderedCells = 0;
U32 GroundCover::smStatRenderedBillboards = 0;
U32 GroundCover::smStatRenderedBatches = 0;
U32 GroundCover::smStatRenderedShapes = 0;
F32 GroundCover::smDensityScale = 1.0f;
ConsoleDocClass( GroundCover,
"@brief Covers the ground in a field of objects (IE: Grass, Flowers, etc)."
"@ingroup Foliage\n"
);
GroundCover::GroundCover()
{
mTypeMask |= StaticObjectType | StaticShapeObjectType;
mNetFlags.set( Ghostable | ScopeAlways );
mRadius = 200.0f;
mZOffset = 0.0f;
mFadeRadius = 50.0f;
mShapeCullRadius = 75.0f;
mShapesCastShadows = true;
mReflectRadiusScale = 0.25f;
mGridSize = 7;
// By initializing this to a big value we
// ensure we warp on first render.
mGridIndex.set( S32_MAX, S32_MAX );
mMaxPlacement = 1000;
mLastPlacementCount = 0;
mDebugRenderCells = false;
mDebugNoBillboards = false;
mDebugNoShapes = false;
mDebugLockFrustum = false;
mRandomSeed = 1;
mMaterial = NULL;
mMatInst = NULL;
mMatParams = NULL;
mTypeRectsParam = NULL;
mFadeParams = NULL;
mWindDirParam = NULL;
mGustInfoParam = NULL;
mTurbInfoParam = NULL;
mCamRightParam = NULL;
mCamUpParam = NULL;
mMaxBillboardTiltAngle = 90.0f;
// TODO: This really doesn't belong here... we need a
// real wind system for Torque scenes. This data
// would be part of a global scene wind or area wind
// emitter.
//
// Tom Spilman - 10/16/2007
mWindGustLength = 20.0f;
mWindGustFrequency = 0.5f;
mWindGustStrength = 0.5f;
mWindDirection.set( 1.0f, 0.0f );
mWindTurbulenceFrequency = 1.2f;
mWindTurbulenceStrength = 0.125f;
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
{
mProbability[i] = 0.0f;
mSizeMin[i] = 1.0f;
mSizeMax[i] = 1.0f;
mSizeExponent[i] = 1.0f;
mWindScale[i] = 1.0f;
mMaxSlope[i] = 0.0f;
mMinElevation[i] = -99999.0f;
mMaxElevation[i] = 99999.0f;
mLayer[i] = StringTable->EmptyString();
mInvertLayer[i] = false;
mMinClumpCount[i] = 1;
mMaxClumpCount[i] = 1;
mClumpCountExponent[i] = 1.0f;
mClumpRadius[i] = 1.0f;
mBillboardRects[i].point.set( 0.0f, 0.0f );
mBillboardRects[i].extent.set( 1.0f, 1.0f );
mShapeFilenames[i] = NULL;
mShapeInstances[i] = NULL;
mBillboardAspectScales[i] = 1.0f;
mNormalizedProbability[i] = 0.0f;
}
}
GroundCover::~GroundCover()
{
SAFE_DELETE( mMatInst );
}
IMPLEMENT_CO_NETOBJECT_V1(GroundCover);
void GroundCover::initPersistFields()
{
addGroup( "GroundCover General" );
addField( "material", TypeMaterialName, Offset( mMaterialName, GroundCover ), "Material used by all GroundCover segments." );
addField( "radius", TypeF32, Offset( mRadius, GroundCover ), "Outer generation radius from the current camera position." );
addField( "dissolveRadius",TypeF32, Offset( mFadeRadius, GroundCover ), "This is less than or equal to radius and defines when fading of cover elements begins." );
addField( "reflectScale", TypeF32, Offset( mReflectRadiusScale, GroundCover ), "Scales the various culling radii when rendering a reflection. Typically for water." );
addField( "gridSize", TypeS32, Offset( mGridSize, GroundCover ), "The number of cells per axis in the grid." );
addField( "zOffset", TypeF32, Offset( mZOffset, GroundCover ), "Offset along the Z axis to render the ground cover." );
addField( "seed", TypeS32, Offset( mRandomSeed, GroundCover ), "This RNG seed is saved and sent to clients for generating the same cover." );
addField( "maxElements", TypeS32, Offset( mMaxPlacement, GroundCover ), "The maximum amount of cover elements to include in the grid at any one time." );
addField( "maxBillboardTiltAngle", TypeF32, Offset( mMaxBillboardTiltAngle, GroundCover ),"The maximum amout of degrees the billboard will tilt down to match the camera." );
addField( "shapeCullRadius", TypeF32, Offset( mShapeCullRadius, GroundCover ), "This is the distance at which DTS elements are completely culled out." );
addField( "shapesCastShadows", TypeBool, Offset( mShapesCastShadows, GroundCover ), "Whether DTS elements should cast shadows or not." );
addArray( "Types", MAX_COVERTYPES );
addField( "billboardUVs", TypeRectUV, Offset( mBillboardRects, GroundCover ), MAX_COVERTYPES, "Subset material UV coordinates for this cover billboard." );
addField( "shapeFilename", TypeFilename, Offset( mShapeFilenames, GroundCover ), MAX_COVERTYPES, "The cover shape filename. [Optional]" );
addField( "layer", TypeTerrainMaterialName, Offset( mLayer, GroundCover ), MAX_COVERTYPES, "Terrain material name to limit coverage to, or blank to not limit." );
addField( "invertLayer", TypeBool, Offset( mInvertLayer, GroundCover ), MAX_COVERTYPES, "Indicates that the terrain material index given in 'layer' is an exclusion mask." );
addField( "probability", TypeF32, Offset( mProbability, GroundCover ), MAX_COVERTYPES, "The probability of one cover type verses another (relative to all cover types)." );
addField( "sizeMin", TypeF32, Offset( mSizeMin, GroundCover ), MAX_COVERTYPES, "The minimum random size for each cover type." );
addField( "sizeMax", TypeF32, Offset( mSizeMax, GroundCover ), MAX_COVERTYPES, "The maximum random size of this cover type." );
addField( "sizeExponent", TypeF32, Offset( mSizeExponent, GroundCover ), MAX_COVERTYPES, "An exponent used to bias between the minimum and maximum random sizes." );
addField( "windScale", TypeF32, Offset( mWindScale, GroundCover ), MAX_COVERTYPES, "The wind effect scale." );
addField( "maxSlope", TypeF32, Offset( mMaxSlope, GroundCover ), MAX_COVERTYPES, "The maximum slope angle in degrees for placement." );
addField( "minElevation", TypeF32, Offset( mMinElevation, GroundCover ), MAX_COVERTYPES, "The minimum world space elevation for placement." );
addField( "maxElevation", TypeF32, Offset( mMaxElevation, GroundCover ), MAX_COVERTYPES, "The maximum world space elevation for placement." );
addField( "minClumpCount", TypeS32, Offset( mMinClumpCount, GroundCover ), MAX_COVERTYPES, "The minimum amount of elements in a clump." );
addField( "maxClumpCount", TypeS32, Offset( mMaxClumpCount, GroundCover ), MAX_COVERTYPES, "The maximum amount of elements in a clump." );
addField( "clumpExponent", TypeF32, Offset( mClumpCountExponent, GroundCover ), MAX_COVERTYPES, "An exponent used to bias between the minimum and maximum clump counts for a particular clump." );
addField( "clumpRadius", TypeF32, Offset( mClumpRadius, GroundCover ), MAX_COVERTYPES, "The maximum clump radius." );
endArray( "Types" );
endGroup( "GroundCover General" );
addGroup( "GroundCover Wind" );
addField( "windDirection", TypePoint2F, Offset( mWindDirection, GroundCover ), "The direction of the wind." );
addField( "windGustLength", TypeF32, Offset( mWindGustLength, GroundCover ), "The length in meters between peaks in the wind gust." );
addField( "windGustFrequency",TypeF32, Offset( mWindGustFrequency, GroundCover ), "Controls how often the wind gust peaks per second." );
addField( "windGustStrength", TypeF32, Offset( mWindGustStrength, GroundCover ), "The maximum distance in meters that the peak wind gust will displace an element." );
addField( "windTurbulenceFrequency", TypeF32, Offset( mWindTurbulenceFrequency, GroundCover ),"Controls the overall rapidity of the wind turbulence." );
addField( "windTurbulenceStrength", TypeF32, Offset( mWindTurbulenceStrength, GroundCover ), "The maximum distance in meters that the turbulence can displace a ground cover element." );
endGroup( "GroundCover Wind" );
addGroup( "GroundCover Debug" );
addField( "lockFrustum", TypeBool, Offset( mDebugLockFrustum, GroundCover ), "Debug parameter for locking the culling frustum which will freeze the cover generation." );
addField( "renderCells", TypeBool, Offset( mDebugRenderCells, GroundCover ), "Debug parameter for displaying the grid cells." );
addField( "noBillboards", TypeBool, Offset( mDebugNoBillboards, GroundCover ), "Debug parameter for turning off billboard rendering." );
addField( "noShapes", TypeBool, Offset( mDebugNoShapes, GroundCover ), "Debug parameter for turning off shape rendering." );
endGroup( "GroundCover Debug" );
Parent::initPersistFields();
}
void GroundCover::consoleInit()
{
Con::addVariable( "$pref::GroundCover::densityScale", TypeF32, &smDensityScale, "A global LOD scalar which can reduce the overall density of placed GroundCover.\n"
"@ingroup Foliage\n");
Con::addVariable( "$GroundCover::renderedCells", TypeS32, &smStatRenderedCells, "Stat for number of rendered cells.\n"
"@ingroup Foliage\n");
Con::addVariable( "$GroundCover::renderedBillboards", TypeS32, &smStatRenderedBillboards, "Stat for number of rendered billboards.\n"
"@ingroup Foliage\n");
Con::addVariable( "$GroundCover::renderedBatches", TypeS32, &smStatRenderedBatches, "Stat for number of rendered billboard batches.\n"
"@ingroup Foliage\n");
Con::addVariable( "$GroundCover::renderedShapes", TypeS32, &smStatRenderedShapes, "Stat for number of rendered shapes.\n"
"@ingroup Foliage\n");
Parent::consoleInit();
}
bool GroundCover::onAdd()
{
if (!Parent::onAdd())
return false;
// We don't use any bounds.
setGlobalBounds();
resetWorldBox();
// Prepare some client side things.
if ( isClientObject() )
{
_initMaterial();
_initShapes();
// Hook ourselves up to get terrain change notifications.
TerrainBlock::smUpdateSignal.notify( this, &GroundCover::onTerrainUpdated );
}
addToScene();
return true;
}
void GroundCover::onRemove()
{
Parent::onRemove();
_deleteCells();
_deleteShapes();
if ( isClientObject() )
{
TerrainBlock::smUpdateSignal.remove( this, &GroundCover::onTerrainUpdated );
}
removeFromScene();
}
void GroundCover::inspectPostApply()
{
Parent::inspectPostApply();
// We flag all the parameters as changed because
// we're feeling lazy and there is not a good way
// to track what parameters changed.
//
// TODO: Add a mask bit option to addField() and/or
// addGroup() which is passed to inspectPostApply
// for detection of changed elements.
//
setMaskBits(U32(-1) );
}
U32 GroundCover::packUpdate( NetConnection *connection, U32 mask, BitStream *stream )
{
U32 retMask = Parent::packUpdate( connection, mask, stream );
if (stream->writeFlag(mask & InitialUpdateMask))
{
// TODO: We could probably optimize a few of these
// based on reasonable units at some point.
stream->write( mMaterialName );
stream->write( mRadius );
stream->write( mZOffset );
stream->write( mFadeRadius );
stream->write( mShapeCullRadius );
stream->writeFlag( mShapesCastShadows );
stream->write( mReflectRadiusScale );
stream->write( mGridSize );
stream->write( mRandomSeed );
stream->write( mMaxPlacement );
stream->write( mMaxBillboardTiltAngle );
stream->write( mWindDirection.x );
stream->write( mWindDirection.y );
stream->write( mWindGustLength );
stream->write( mWindGustFrequency );
stream->write( mWindGustStrength );
stream->write( mWindTurbulenceFrequency );
stream->write( mWindTurbulenceStrength );
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
{
stream->write( mProbability[i] );
stream->write( mSizeMin[i] );
stream->write( mSizeMax[i] );
stream->write( mSizeExponent[i] );
stream->write( mWindScale[i] );
stream->write( mMaxSlope[i] );
stream->write( mMinElevation[i] );
stream->write( mMaxElevation[i] );
stream->writeString( mLayer[i] );
stream->writeFlag( mInvertLayer[i] );
stream->write( mMinClumpCount[i] );
stream->write( mMaxClumpCount[i] );
stream->write( mClumpCountExponent[i] );
stream->write( mClumpRadius[i] );
stream->write( mBillboardRects[i].point.x );
stream->write( mBillboardRects[i].point.y );
stream->write( mBillboardRects[i].extent.x );
stream->write( mBillboardRects[i].extent.y );
stream->writeString( mShapeFilenames[i] );
}
stream->writeFlag( mDebugRenderCells );
stream->writeFlag( mDebugNoBillboards );
stream->writeFlag( mDebugNoShapes );
stream->writeFlag( mDebugLockFrustum );
}
return retMask;
}
void GroundCover::unpackUpdate( NetConnection *connection, BitStream *stream )
{
Parent::unpackUpdate( connection, stream );
if (stream->readFlag())
{
stream->read( &mMaterialName );
stream->read( &mRadius );
stream->read( &mZOffset );
stream->read( &mFadeRadius );
stream->read( &mShapeCullRadius );
mShapesCastShadows = stream->readFlag();
stream->read( &mReflectRadiusScale );
stream->read( &mGridSize );
stream->read( &mRandomSeed );
stream->read( &mMaxPlacement );
stream->read( &mMaxBillboardTiltAngle );
stream->read( &mWindDirection.x );
stream->read( &mWindDirection.y );
stream->read( &mWindGustLength );
stream->read( &mWindGustFrequency );
stream->read( &mWindGustStrength );
stream->read( &mWindTurbulenceFrequency );
stream->read( &mWindTurbulenceStrength );
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
{
stream->read( &mProbability[i] );
stream->read( &mSizeMin[i] );
stream->read( &mSizeMax[i] );
stream->read( &mSizeExponent[i] );
stream->read( &mWindScale[i] );
stream->read( &mMaxSlope[i] );
stream->read( &mMinElevation[i] );
stream->read( &mMaxElevation[i] );
mLayer[i] = stream->readSTString();
mInvertLayer[i] = stream->readFlag();
stream->read( &mMinClumpCount[i] );
stream->read( &mMaxClumpCount[i] );
stream->read( &mClumpCountExponent[i] );
stream->read( &mClumpRadius[i] );
stream->read( &mBillboardRects[i].point.x );
stream->read( &mBillboardRects[i].point.y );
stream->read( &mBillboardRects[i].extent.x );
stream->read( &mBillboardRects[i].extent.y );
mShapeFilenames[i] = stream->readSTString();
}
mDebugRenderCells = stream->readFlag();
mDebugNoBillboards = stream->readFlag();
mDebugNoShapes = stream->readFlag();
mDebugLockFrustum = stream->readFlag();
// We have no way to easily know what changed, so by clearing
// the cells we force a reinit and regeneration of the cells.
// It's sloppy, but it works for now.
_freeCells();
if ( isProperlyAdded() )
_initMaterial();
}
}
void GroundCover::_initMaterial()
{
SAFE_DELETE( mMatInst );
if ( mMaterialName.isNotEmpty() )
if ( !Sim::findObject( mMaterialName, mMaterial ) )
Con::errorf( "GroundCover::_initMaterial - Material %s was not found.", mMaterialName.c_str() );
if ( mMaterial )
mMatInst = mMaterial->createMatInstance();
else
mMatInst = MATMGR->createMatInstance( "WarningMaterial" );
// Add our special feature that makes it all work...
FeatureSet features = MATMGR->getDefaultFeatures();
features.addFeature( MFT_Foliage );
// Our feature requires a pointer back to this object
// to properly setup its shader consts.
mMatInst->setUserObject( this );
// DO IT!
mMatInst->init( features, getGFXVertexFormat<GCVertex>() );
}
void GroundCover::_initShapes()
{
_deleteShapes();
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
{
if ( !mShapeFilenames[i] || !mShapeFilenames[i][0] )
continue;
// Load the shape.
Resource<TSShape> shape = ResourceManager::get().load(mShapeFilenames[i]);
if ( !(bool)shape )
{
Con::warnf( "GroundCover::_initShapes() unable to load shape: %s", mShapeFilenames[i] );
continue;
}
if ( isClientObject() && !shape->preloadMaterialList(shape.getPath()) && NetConnection::filesWereDownloaded() )
{
Con::warnf( "GroundCover::_initShapes() material preload failed for shape: %s", mShapeFilenames[i] );
continue;
}
// Create the shape instance.
mShapeInstances[i] = new TSShapeInstance( shape, isClientObject() );
}
}
void GroundCover::_deleteShapes()
{
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
{
delete mShapeInstances[i];
mShapeInstances[i] = NULL;
}
}
void GroundCover::_deleteCells()
{
// Delete the allocation list.
for ( S32 i=0; i < mAllocCellList.size(); i++ )
delete mAllocCellList[i];
mAllocCellList.clear();
// Zero out the rest of the stuff.
_freeCells();
}
void GroundCover::_freeCells()
{
// Zero the grid and scratch space.
mCellGrid.clear();
mScratchGrid.clear();
// Compact things... remove excess allocated cells.
const U32 maxCells = mGridSize * mGridSize;
if ( mAllocCellList.size() > maxCells )
{
for ( S32 i=maxCells; i < mAllocCellList.size(); i++ )
delete mAllocCellList[i];
mAllocCellList.setSize( maxCells );
}
// Move all the alloced cells into the free list.
mFreeCellList.clear();
mFreeCellList.merge( mAllocCellList );
// Release the primitive buffer.
mPrimBuffer = NULL;
}
void GroundCover::_recycleCell( GroundCoverCell* cell )
{
mFreeCellList.push_back( cell );
}
void GroundCover::_initialize( U32 cellCount, U32 cellPlacementCount )
{
// Cleanup everything... we're starting over.
_freeCells();
_deleteShapes();
// Nothing to do without a count!
if ( cellPlacementCount == 0 )
return;
// Reset the grid sizes.
mCellGrid.setSize( cellCount );
dMemset( mCellGrid.address(), 0, mCellGrid.memSize() );
mScratchGrid.setSize( cellCount );
// Rebuild the texture aspect scales for each type.
F32 textureAspect = 1.0f;
if( mMatInst && mMatInst->isValid())
{
Material* mat = dynamic_cast<Material*>(mMatInst->getMaterial());
if(mat)
{
GFXTexHandle tex(mat->mDiffuseMapFilename[0], &GFXDefaultStaticDiffuseProfile, "GroundCover texture aspect ratio check" );
if(tex.isValid())
{
U32 w = tex.getWidth();
U32 h = tex.getHeight();
if(h > 0)
textureAspect = F32(w) / F32(h);
}
}
}
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
{
if ( mBillboardRects[i].extent.y > 0.0f )
{
mBillboardAspectScales[i] = textureAspect * mBillboardRects[i].extent.x / mBillboardRects[i].extent.y;
}
else
mBillboardAspectScales[i] = 0.0f;
}
// Load the shapes again.
_initShapes();
// Set the primitive buffer up for the maximum placement in a cell.
mPrimBuffer.set( GFX, cellPlacementCount * 6, 0, GFXBufferTypeStatic );
U16 *idxBuff;
mPrimBuffer.lock(&idxBuff);
for ( U32 i=0; i < cellPlacementCount; i++ )
{
//
// The vertex pattern in the VB for each
// billboard is as follows...
//
// 0----1
// |\ |
// | \ |
// | \ |
// | \|
// 3----2
//
// We setup the index order below to ensure
// sequential, cache friendly, access.
//
U32 offset = i * 4;
idxBuff[i*6+0] = 0 + offset;
idxBuff[i*6+1] = 1 + offset;
idxBuff[i*6+2] = 2 + offset;
idxBuff[i*6+3] = 2 + offset;
idxBuff[i*6+4] = 3 + offset;
idxBuff[i*6+5] = 0 + offset;
}
mPrimBuffer.unlock();
// Generate the normalized probability.
F32 total = 0.0f;
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
{
// If the element isn't gonna render... then
// set the probability to zero.
if ( mShapeInstances[i] == NULL && mBillboardAspectScales[i] <= 0.0001f )
{
mNormalizedProbability[i] = 0.0f;
}
else
{
mNormalizedProbability[i] = mProbability[i];
total += mProbability[i];
}
}
if ( total > 0.0f )
{
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
mNormalizedProbability[i] /= total;
}
}
GroundCoverCell* GroundCover::_generateCell( const Point2I& index,
const Box3F& bounds,
U32 placementCount,
S32 randSeed )
{
PROFILE_SCOPE(GroundCover_GenerateCell);
const Vector<SceneObject*> terrainBlocks = getContainer()->getTerrains();
if ( terrainBlocks.empty() )
return NULL;
// Grab a free cell or allocate a new one.
GroundCoverCell* cell;
if ( mFreeCellList.empty() )
{
cell = new GroundCoverCell();
mAllocCellList.push_back( cell );
}
else
{
cell = mFreeCellList.last();
mFreeCellList.pop_back();
}
cell->mDirty = true;
cell->mIndex = index;
cell->mBounds = bounds;
Point3F pos( 0, 0, 0 );
Box3F renderBounds = bounds;
Point3F point;
Point3F normal;
F32 h;
Point2F cp, uv;
bool hit;
GroundCoverCell::Placement p;
F32 rotation;
F32 size;
F32 sizeExponent;
Point2I lpos;
//F32 value;
VectorF right;
StringTableEntry matName = StringTable->EmptyString();
bool firstElem = true;
TerrainBlock *terrainBlock = NULL;
cell->mBillboards.clear();
cell->mBillboards.reserve( placementCount );
cell->mShapes.clear();
cell->mShapes.reserve( placementCount );
F32 terrainSquareSize,
oneOverTerrainLength,
oneOverTerrainSquareSize;
const GBitmap* terrainLM = NULL;
// The RNG that we'll use in generation.
MRandom rand( 0 );
// We process one type at a time.
for ( U32 type=0; type < MAX_COVERTYPES; type++ )
{
// How many cover elements do we need to generate for this type?
const S32 typeCount = mNormalizedProbability[type] * (F32)placementCount;
if ( typeCount <= 0 )
continue;
// Grab the terrain layer for this type.
/*
const TerrainDataLayer* dataLayer = NULL;
const bool typeInvertLayer = mInvertLayer[type];
if ( mLayer[type] > -1 )
{
dataLayer = mTerrainBlock->getDataLayer( mLayer[type] );
if ( dataLayer )
{
// Do an initial check to see if we can place any place anything
// at all... if the layer area for this element is empty then
// there is nothing more to do.
RectI area( (S32)mFloor( ( bounds.minExtents.x - pos.x ) * oneOverTerrainSquareSize ),
(S32)mFloor( ( bounds.minExtents.y - pos.y ) * oneOverTerrainSquareSize ),
(S32)mCeil( ( bounds.maxExtents.x - pos.x ) * oneOverTerrainSquareSize ),
(S32)mCeil( ( bounds.maxExtents.y - pos.y ) * oneOverTerrainSquareSize ) );
area.extent -= area.point;
if ( dataLayer->testFill( area, typeInvertLayer ? 255 : 0 ) )
continue;
}
}
// If the layer is not inverted and we have no data
// then we have nothing to draw.
if ( !typeInvertLayer && !dataLayer )
continue;
*/
// We set the seed we were passed which is based on this grids position
// in the world and add the type value. This keeps changes to one type
// from effecting the outcome of the others.
rand.setSeed( randSeed + type );
// Setup for doing clumps.
S32 clumps = 0;
Point2F clumpCenter(0.0f, 0.0f);
const S32 clumpMin = getMax( 1, (S32)mMinClumpCount[type] );
F32 clumpExponent;
// We mult this by -1 each billboard we make then use
// it to scale the billboard x axis to flip them. This
// essentially gives us twice the variation for free.
F32 flipBB = -1.0f;
// Precompute a few other type specific values.
const F32 typeSizeRange = mSizeMax[type] - mSizeMin[type];
const F32 typeMaxSlope = mMaxSlope[type];
const F32 typeMaxElevation = mMaxElevation[type];
const F32 typeMinElevation = mMinElevation[type];
const bool typeIsShape = mShapeInstances[ type ] != NULL;
const Box3F typeShapeBounds = typeIsShape ? mShapeInstances[ type ]->getShape()->bounds : Box3F();
const F32 typeWindScale = mWindScale[type];
StringTableEntry typeLayer = mLayer[type];
const bool typeInvertLayer = mInvertLayer[type];
// We can set this once here... all the placements for this are the same.
p.type = type;
p.windAmplitude = typeWindScale;
p.lmColor.set(1.0f,1.0f,1.0f);
// Generate all the cover elements for this type.
for ( S32 i=0; i < typeCount; i++ )
{
// Do all the other random things here first as to not
// disturb the random sequence if the terrain geometry
// or cover layers change.
// Get the random position.
cp.set( rand.randF(), rand.randF() );
// Prepare the clump info.
clumpExponent = mClampF( mPow( rand.randF(), mClumpCountExponent[type] ), 0.0f, 1.0f );
if ( clumps <= 0 )
{
// We're starting a new clump.
clumps = ( clumpMin + mFloor( ( mMaxClumpCount[type] - clumpMin ) * clumpExponent ) ) - 1;
cp.set( bounds.minExtents.x + cp.x * bounds.len_x(),
bounds.minExtents.y + cp.y * bounds.len_y() );
clumpCenter = cp;
}
else
{
clumps--;
cp.set( clumpCenter.x - ( ( cp.x - 0.5f ) * mClumpRadius[type] ),
clumpCenter.y - ( ( cp.y - 0.5f ) * mClumpRadius[type] ) );
}
// Which terrain do I place on?
if ( terrainBlocks.size() == 1 )
terrainBlock = dynamic_cast< TerrainBlock* >( terrainBlocks.first() );
else
{
for ( U32 i = 0; i < terrainBlocks.size(); i++ )
{
TerrainBlock *terrain = dynamic_cast< TerrainBlock* >( terrainBlocks[ i ] );
if( !terrain )
continue;
const Box3F &terrBounds = terrain->getWorldBox();
if ( cp.x < terrBounds.minExtents.x || cp.x > terrBounds.maxExtents.x ||
cp.y < terrBounds.minExtents.y || cp.y > terrBounds.maxExtents.y )
continue;
terrainBlock = terrain;
break;
}
}
// This should only happen if the generation went off
// the edge of the terrain blocks.
if ( !terrainBlock )
continue;
terrainLM = terrainBlock->getLightMap();
pos = terrainBlock->getPosition();
terrainSquareSize = (F32)terrainBlock->getSquareSize();
oneOverTerrainLength = 1.0f / terrainBlock->getWorldBlockSize();
oneOverTerrainSquareSize = 1.0f / terrainSquareSize;
// The size is calculated using an exponent to control
// the frequency between min and max sizes.
sizeExponent = mClampF( mPow( rand.randF(), mSizeExponent[type] ), 0.0f, 1.0f );
size = mSizeMin[type] + ( typeSizeRange * sizeExponent );
// Generate a random z rotation.
rotation = rand.randF() * M_2PI_F;
// Flip the billboard now for the next generation.
flipBB *= -1.0f;
PROFILE_START( GroundCover_TerrainRayCast );
hit = terrainBlock->getNormalHeightMaterial( Point2F( cp.x - pos.x, cp.y - pos.y ),
&normal, &h, matName );
// TODO: When did we loose the world space elevation when
// getting the terrain height?
h += pos.z + mZOffset;
PROFILE_END(); // GroundCover_TerrainRayCast
if ( !hit || h > typeMaxElevation || h < typeMinElevation ||
( typeLayer[0] && !typeInvertLayer && matName != typeLayer ) ||
( typeLayer[0] && typeInvertLayer && matName == typeLayer ) )
continue;
// Do we need to check slope?
if ( !mIsZero( typeMaxSlope ) )
{
if (mAcos(normal.z) > mDegToRad(typeMaxSlope))
continue;
}
point.set( cp.x, cp.y, h );
p.point = point;
p.rotation = rotation;
p.normal = normal;
// Grab the terrain lightmap color at this position.
//
// TODO: Can't we remove this test? The terrain
// lightmap should never be null... NEVER!
//
if ( terrainLM )
{
// TODO: We could probably call terrainLM->getBits()
// once outside the loop then pre-calculate the scalar
// for converting a world position into a lexel...
// avoiding the extra protections inside of sampleTexel().
uv.x = (point.x + pos.x) * oneOverTerrainLength;
uv.y = (point.y + pos.y) * oneOverTerrainLength;
uv.x -= mFloor(uv.x);
uv.y -= mFloor(uv.y);
p.lmColor = terrainLM->sampleTexel(uv.x,uv.y);
}
// Put it into the right list by type.
//
// TODO: Could we break up the generation into
// two separate loops for shapes and billboards
// and gain performance?
//
if ( typeIsShape )
{
// TODO: Convert the size into a real size... not scale!
// TODO: We could probably cache the shape bounds
// into a primitive array and avoid the double pointer
// dereference per placement.
p.size.set( size, size, size );
p.worldBox = typeShapeBounds;
p.worldBox.minExtents *= size;
p.worldBox.maxExtents *= size;
p.worldBox.minExtents += point;
p.worldBox.maxExtents += point;
cell->mShapes.push_back( p );
}
else
{
p.size.y = size;
p.size.x = size * flipBB * mBillboardAspectScales[type];
p.worldBox.maxExtents = p.worldBox.minExtents = point;
cell->mBillboards.push_back( p );
}
// Update the render bounds.
if ( firstElem )
{
renderBounds = p.worldBox;
firstElem = false;
}
else
{
renderBounds.extend( p.worldBox.minExtents );
renderBounds.extend( p.worldBox.maxExtents );
}
} // for ( S32 i=0; i < typeCount; i++ )
} // for ( U32 type=0; type < NumCoverTypes; type++ )
cell->mRenderBounds = renderBounds;
cell->mBounds.minExtents.z = renderBounds.minExtents.z;
cell->mBounds.maxExtents.z = renderBounds.maxExtents.z;
return cell;
}
void GroundCover::onTerrainUpdated( U32 flags, TerrainBlock *tblock, const Point2I& min, const Point2I& max )
{
if ( isServerObject() )
return;
// Free all the cells if we've gotten a lightmap update.
if ( flags & TerrainBlock::LightmapUpdate )
{
_freeCells();
return;
}
// TODO: EmptyUpdate doesn't work yet... fix editor/terrain.
// If this is a height or opacity update only clear
// the cells that have changed.
if ( flags & TerrainBlock::HeightmapUpdate ||
flags & TerrainBlock::LayersUpdate ||
flags & TerrainBlock::EmptyUpdate )
{
// 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, 0.0f,
F32( max.x * size ) + pos.x, F32( max.y * size ) + pos.y, 0.0f );
// Now free any cells that overlap it!
for ( S32 i = 0; i < mCellGrid.size(); i++ )
{
GroundCoverCell* cell = mCellGrid[ i ];
if ( !cell )
continue;
const Box3F& bounds = cell->getBounds();
dirty.minExtents.z = bounds.minExtents.z;
dirty.maxExtents.z = bounds.maxExtents.z;
if ( bounds.isOverlapped( dirty ) )
{
mCellGrid[ i ] = NULL;
_recycleCell( cell );
}
}
}
}
void GroundCover::_updateCoverGrid( const Frustum &culler )
{
PROFILE_SCOPE( GroundCover_UpdateCoverGrid );
mGridSize = getMax( mGridSize, (U32)2 );
// How many cells in the grid?
const U32 cells = mGridSize * mGridSize;
// Whats the max placement count for each cell considering
// the grid size and quality scale LOD value.
const S32 placementCount = getMax( ( (F32)mMaxPlacement * smDensityScale ) / F32( mGridSize * mGridSize ), 0.0f );
// If the cell grid isn't sized or the placement count
// changed (most likely because of quality lod) then we
// need to initialize the system again.
if ( mCellGrid.empty() || placementCount != mLastPlacementCount )
{
_initialize( cells, placementCount );
mLastPlacementCount = placementCount;
}
// Without a count... we don't function at all.
if ( placementCount == 0 )
return;
// Clear the scratch grid.
dMemset( mScratchGrid.address(), 0, mScratchGrid.memSize() );
// Calculate the normal cell size here.
const F32 cellSize = ( mRadius * 2.0f ) / (F32)(mGridSize - 1);
// Figure out the root index of the new grid based on the camera position.
Point2I index( (S32)mFloor( ( culler.getPosition().x - mRadius ) / cellSize ),
(S32)mFloor( ( culler.getPosition().y - mRadius ) / cellSize ) );
// Figure out the cell shift between the old and new grid positions.
Point2I shift = mGridIndex - index;
// If we've shifted more than one in either axis then we've warped.
bool didWarp = shift.x > 1 || shift.x < -1 ||
shift.y > 1 || shift.y < -1 ? true : false;
// Go thru the grid shifting each cell we find and
// placing them in the scratch grid.
for ( S32 i = 0; i < mCellGrid.size(); i++ )
{
GroundCoverCell* cell = mCellGrid[ i ];
if ( !cell )
continue;
// Whats our new index?
Point2I newIndex = cell->shiftIndex( shift );
// Is this cell outside of the new grid?
if ( newIndex.x < 0 || newIndex.x >= mGridSize ||
newIndex.y < 0 || newIndex.y >= mGridSize )
{
_recycleCell( cell );
continue;
}
// Place the cell in the scratch grid.
mScratchGrid[ ( newIndex.y * mGridSize ) + newIndex.x ] = cell;
}
// Get the terrain elevation range for setting the default cell bounds.
F32 terrainMinHeight = -5000.0f,
terrainMaxHeight = 5000.0f;
// Go thru the scratch grid copying each cell back to the
// cell grid and creating new cells as needed.
//
// By limiting ourselves to only one new cell generation per
// update we're lowering the performance hiccup during movement
// without getting into the complexity of threading. The delay
// in generation is rarely noticeable in normal play.
//
// The only caveat is that we need to generate the entire visible
// grid when we warp.
U32 cellsGenerated = 0;
for ( S32 i = 0; i < mScratchGrid.size(); i++ )
{
GroundCoverCell* cell = mScratchGrid[ i ];
if ( !cell && ( cellsGenerated == 0 || didWarp ) )
{
// Get the index point of this new cell.
S32 y = i / mGridSize;
S32 x = i - ( y * mGridSize );
Point2I newIndex = index + Point2I( x, y );
// What will be the world placement bounds for this cell.
Box3F bounds;
bounds.minExtents.set( newIndex.x * cellSize, newIndex.y * cellSize, terrainMinHeight );
bounds.maxExtents.set( bounds.minExtents.x + cellSize, bounds.minExtents.y + cellSize, terrainMaxHeight );
if ( mCuller.isCulled( bounds ) )
{
mCellGrid[ i ] = NULL;
continue;
}
// We need to allocate a new cell.
//
// TODO: This is the expensive call and where we should optimize. In
// particular the next best optimization would be to take advantage of
// multiple cores so that we can generate all the cells in one update.
//
// Instead of generating the cell here we would allocate a cell and stick
// it into a thread safe queue (maybe lockless) as well as the mCellGrid.
// Once all were allocated we would do something like this...
//
// TorqueParallelProcess( cellsToGenerateQueue, _generateCell );
//
// Internally this function would pass the queue to some global pre-allocated
// worker threads which are locked to a particular core. While the main
// thread waits for the worker threads to finish it will process cells itself.
//
cell = _generateCell( newIndex - index,
bounds,
placementCount,
mRandomSeed + mAbs( newIndex.x ) + mAbs( newIndex.y ) );
// Increment our generation count.
if ( cell )
++cellsGenerated;
}
mCellGrid[ i ] = cell;
}
// Store the new grid index.
mGridIndex = index;
}
void GroundCover::prepRenderImage( SceneRenderState *state )
{
// Reset stats each time we hit the diffuse pass.
if( state->isDiffusePass() )
{
smStatRenderedCells = 0;
smStatRenderedBillboards = 0;
smStatRenderedBatches = 0;
smStatRenderedShapes = 0;
}
// TODO: Make sure that the ground cover stops rendering
// if you're inside a zoned interior.
if ( state->isReflectPass() && mReflectRadiusScale <= 0.0f )
return;
// Nothing to do if this is a shadow pass and shapes in this GoundCover
// should not be casting shadows.
if( state->isShadowPass() && !mShapesCastShadows )
return;
GFXTransformSaver saver;
// Setup the frustum culler.
if ( ( mCuller.getPosition().isZero() || !mDebugLockFrustum ) && !state->isShadowPass() )
mCuller = state->getFrustum();
// Update the cells, but only during the diffuse pass.
// We don't want cell generation to thrash when the reflection camera
// position doesn't match the diffuse camera!
if ( state->isDiffusePass() )
_updateCoverGrid( mCuller );
// Render billboards but not into shadow passes.
if ( !state->isShadowPass() && mMatInst->isValid() && !mDebugNoBillboards )
{
PROFILE_SCOPE( GroundCover_RenderBillboards );
// Take zoom into account.
F32 screenScale = state->getWorldToScreenScale().y / state->getViewport().extent.y;
// Set the far distance for billboards.
mCuller.setFarDist( mRadius * screenScale );
F32 cullScale = 1.0f;
if ( state->isReflectPass() )
cullScale = mReflectRadiusScale;
// Setup our shader const data.
// Must be done prior to submitting our render instance.
mShaderConstData.fadeInfo.set( mFadeRadius * cullScale * screenScale, mRadius * cullScale * screenScale );
const F32 simTime = Sim::getCurrentTime() * 0.001f;
mShaderConstData.gustInfo.set( mWindGustLength, mWindGustFrequency * simTime, mWindGustStrength );
mShaderConstData.turbInfo.set( mWindTurbulenceFrequency * simTime, mWindTurbulenceStrength );
const MatrixF &camMat = state->getDiffuseCameraTransform();
Point3F camDir, camUp, camRight;
camMat.getColumn( 1, &camDir );
camMat.getColumn( 2, &camUp );
camMat.getColumn( 0, &camRight );
// Limit the camera up vector to keep the billboards
// from leaning too far down into the terrain.
VectorF lookDir( camDir.x, camDir.y, 0.0f );
F32 angle;
if ( !lookDir.isZero() )
{
lookDir.normalize();
angle = mAcos( mDot( camUp, lookDir ) );
}
else
{
angle = camDir.z < 0.0f ? 0.0f : ( M_PI_F / 2.0f );
}
const F32 maxBillboardTiltRads = mDegToRad( mMaxBillboardTiltAngle );
if ( angle < (M_PI_F / 2.0f) - maxBillboardTiltRads )
{
QuatF quat( AngAxisF( camRight, maxBillboardTiltRads ) );
quat.mulP( VectorF( 0.0f, 0.0f, 1.0f ), &camUp );
}
mShaderConstData.camRight = camRight;
mShaderConstData.camUp = camUp;
// Cull and submit render instances for cells.
for ( S32 i = 0; i < mCellGrid.size(); i++ )
{
GroundCoverCell* cell = mCellGrid[ i ];
if ( !cell )
continue;
if ( mCuller.isCulled( cell->getRenderBounds() ) )
continue;
cell->renderBillboards( state, mMatInst, &mPrimBuffer );
}
}
// Render TS shapes.
if ( !mDebugNoShapes )
{
// Prepare to render the grid shapes.
PROFILE_SCOPE(GroundCover_RenderShapes);
// Set up our TS render state.
TSRenderState rdata;
rdata.setSceneState( state );
// We might have some forward lit materials
// so pass down a query to gather lights.
LightQuery query;
rdata.setLightQuery( &query );
// TODO: Add a special fade out for DTS?
mCuller.setFarDist( mShapeCullRadius );
for ( S32 i = 0; i < mCellGrid.size(); i++ )
{
GroundCoverCell* cell = mCellGrid[ i ];
if ( !cell || mDebugNoShapes )
continue;
const Box3F &renderBounds = cell->getRenderBounds();
U32 clipMask = mCuller.testPlanes( renderBounds, Frustum::PlaneMaskAll );
if ( clipMask == -1 )
continue;
smStatRenderedCells++;
// Use the cell bounds as the light query volume.
//
// This means all forward lit items in this cell will
// get the same lights, but it performs much better.
query.init( renderBounds );
// Render the shapes in this cell... only pass the culler if the
// cell wasn't fully within the frustum.
smStatRenderedShapes += cell->renderShapes(
rdata,
clipMask != 0 ? &mCuller : NULL,
mShapeInstances );
}
}
if ( mDebugRenderCells )
{
RenderPassManager *pass = state->getRenderPass();
ObjectRenderInst *ri = pass->allocInst<ObjectRenderInst>();
ri->type = RenderPassManager::RIT_Editor;
ri->renderDelegate.bind( this, &GroundCover::_debugRender );
pass->addInst( ri );
}
}
void GroundCover::_debugRender( ObjectRenderInst *ri, SceneRenderState *state, BaseMatInstance *overrideMat )
{
GFXDrawUtil* drawer = GFX->getDrawUtil();
GFXStateBlockDesc desc;
desc.setZReadWrite( true, false );
desc.setBlend( true );
desc.fillMode = GFXFillWireframe;
for ( S32 i = 0; i < mCellGrid.size(); i++ )
{
GroundCoverCell* cell = mCellGrid[ i ];
if ( !cell || ( cell->mBillboards.size() + cell->mShapes.size() ) == 0 )
continue;
if ( mCuller.isCulled( cell->getRenderBounds() ) )
continue;
drawer->drawCube( desc, cell->getRenderBounds().getExtents(), cell->getRenderBounds().getCenter(), ColorI( 0, 255, 0 ) );
}
}