mirror of
https://github.com/TorqueGameEngines/Torque3D.git
synced 2026-01-19 20:24:49 +00:00
while it still remains a good idea to port as many NULL compares and assignments over to nullPtr as feasable, we do still need to sort out how to better support scripted empty, false, and zero assigns for things like objectIDs. this means we'll need to both fully convert the backend of the parser to support that kind of thing, but also alter most if not all exisiting NULLs. up to and including things like SAFE_DELETE. while that's certainly feasable, given there's aproximatel 400 nullptr assigns/checks prior to this commit, and roughly 1800 of the prior, if it terminates in a script call and not an aip one direct, we'll be dialing that back until such time as fork fully fopcused on converting and resolving any lingering mismatches is completed.
1771 lines
61 KiB
C++
1771 lines
61 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/renderDeferredMgr.h"
|
|
#include "console/engineAPI.h"
|
|
#include "T3D/assets/MaterialAsset.h"
|
|
#include "T3D/assets/TerrainMaterialAsset.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()
|
|
: mGroundCover( NULL ),
|
|
mTypeRectsSC( NULL ),
|
|
mFadeSC( NULL ),
|
|
mWindDirSC( NULL ),
|
|
mGustInfoSC( NULL ),
|
|
mTurbInfoSC( NULL ),
|
|
mCamRightSC( NULL ),
|
|
mCamUpSC( 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;
|
|
LinearColorF 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() : mDirty(false) {}
|
|
|
|
~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;
|
|
color = LinearColorF((*iter).lmColor).toColorI();
|
|
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(inst.normal.x, inst.normal.y, 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;
|
|
F32 GroundCover::smFadeScale = 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;
|
|
|
|
INIT_ASSET(Material);
|
|
mMaterialInst = 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;
|
|
|
|
mMinSlope[i] = 0.0f;
|
|
mMaxSlope[i] = 0.0f;
|
|
|
|
mConformToNormal[i] = false;
|
|
mMinRotX[i] = 0.0f;
|
|
mMaxRotX[i] = 0.0f;
|
|
mMinRotY[i] = 0.0f;
|
|
mMaxRotY[i] = 0.0f;
|
|
|
|
mMinElevation[i] = -99999.0f;
|
|
mMaxElevation[i] = 99999.0f;
|
|
|
|
mLayerAsset[i] = NULL;
|
|
mLayerFile[i] = StringTable->EmptyString();
|
|
|
|
mShapeAsset[i] = NULL;
|
|
mShapeFile[i] = StringTable->EmptyString();
|
|
|
|
mInvertLayer[i] = NULL;
|
|
|
|
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 );
|
|
|
|
mShapeAsset[i].registerRefreshNotify(this);
|
|
|
|
mShapeInstances[i] = NULL;
|
|
|
|
mBillboardAspectScales[i] = 1.0f;
|
|
|
|
mNormalizedProbability[i] = 0.0f;
|
|
}
|
|
}
|
|
|
|
GroundCover::~GroundCover()
|
|
{
|
|
SAFE_DELETE( mMaterialInst );
|
|
}
|
|
|
|
IMPLEMENT_CO_NETOBJECT_V1(GroundCover);
|
|
|
|
void GroundCover::initPersistFields()
|
|
{
|
|
docsURL;
|
|
addGroup( "GroundCover General" );
|
|
|
|
INITPERSISTFIELD_MATERIALASSET(Material, GroundCover, "Material used by all GroundCover segments.");
|
|
|
|
addFieldV( "radius", TypeRangedF32, Offset( mRadius, GroundCover ), &CommonValidators::PositiveFloat, "Outer generation radius from the current camera position." );
|
|
addFieldV( "dissolveRadius", TypeRangedF32, Offset( mFadeRadius, GroundCover ), &CommonValidators::PositiveFloat, "This is less than or equal to radius and defines when fading of cover elements begins." );
|
|
addFieldV( "reflectScale", TypeRangedF32, Offset( mReflectRadiusScale, GroundCover ), &CommonValidators::PositiveFloat, "Scales the various culling radii when rendering a reflection. Typically for water." );
|
|
|
|
addFieldV( "gridSize", TypeRangedS32, Offset( mGridSize, GroundCover ), &CommonValidators::PositiveInt, "The number of cells per axis in the grid." );
|
|
addFieldV( "zOffset", TypeRangedF32, Offset( mZOffset, GroundCover ), &CommonValidators::F32Range, "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." );
|
|
addFieldV( "maxElements", TypeRangedS32, Offset( mMaxPlacement, GroundCover ), &CommonValidators::PositiveInt, "The maximum amount of cover elements to include in the grid at any one time." );
|
|
|
|
addFieldV( "maxBillboardTiltAngle", TypeRangedF32, Offset( mMaxBillboardTiltAngle, GroundCover ), &CommonValidators::PosDegreeRangeHalf,"The maximum amout of degrees the billboard will tilt down to match the camera." );
|
|
addFieldV( "shapeCullRadius", TypeRangedF32, Offset( mShapeCullRadius, GroundCover ), &CommonValidators::PositiveFloat, "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." );
|
|
|
|
INITPERSISTFIELD_SHAPEASSET_ARRAY_REFACTOR(Shape, MAX_COVERTYPES, GroundCover, "The cover shape. [Optional]");
|
|
|
|
INITPERSISTFIELD_TERRAINMATERIALASSET_ARRAY(Layer, MAX_COVERTYPES, GroundCover, "Terrain material assetId to limit coverage to, or blank to not limit.");
|
|
|
|
//Legacy field
|
|
addProtectedField("layer", TypeTerrainMaterialAssetPtr, Offset(mLayerAsset, GroundCover), &_setLayerData, &defaultProtectedGetFn, MAX_COVERTYPES, "Terrain material assetId to limit coverage to, or blank to not limit.", AbstractClassRep::FIELD_HideInInspectors);
|
|
|
|
addField( "invertLayer", TypeBool, Offset( mInvertLayer, GroundCover ), MAX_COVERTYPES, "Indicates that the terrain material index given in 'layer' is an exclusion mask." );
|
|
|
|
addFieldV( "probability", TypeRangedF32, Offset( mProbability, GroundCover ), &CommonValidators::PositiveFloat, MAX_COVERTYPES, "The probability of one cover type verses another (relative to all cover types)." );
|
|
|
|
addFieldV( "sizeMin", TypeRangedF32, Offset( mSizeMin, GroundCover ), &CommonValidators::PositiveFloat, MAX_COVERTYPES, "The minimum random size for each cover type." );
|
|
|
|
addFieldV( "sizeMax", TypeRangedF32, Offset( mSizeMax, GroundCover ), &CommonValidators::PositiveFloat, MAX_COVERTYPES, "The maximum random size of this cover type." );
|
|
|
|
addFieldV( "sizeExponent", TypeRangedF32, Offset( mSizeExponent, GroundCover ), &CommonValidators::PositiveFloat, MAX_COVERTYPES, "An exponent used to bias between the minimum and maximum random sizes." );
|
|
|
|
addFieldV( "windScale", TypeRangedF32, Offset( mWindScale, GroundCover ), &CommonValidators::PositiveFloat, MAX_COVERTYPES, "The wind effect scale." );
|
|
|
|
addFieldV( "minSlope", TypeRangedF32, Offset(mMinSlope, GroundCover), &CommonValidators::PosDegreeRangeQuarter, MAX_COVERTYPES, "The minimum slope angle in degrees for placement.");
|
|
|
|
addFieldV( "maxSlope", TypeRangedF32, Offset( mMaxSlope, GroundCover ), &CommonValidators::PosDegreeRangeQuarter, MAX_COVERTYPES, "The maximum slope angle in degrees for placement." );
|
|
|
|
addField("conformToNormal",TypeBool, Offset(mConformToNormal, GroundCover), MAX_COVERTYPES, "Use the terrain's slope for angle");
|
|
addFieldV("minRotX", TypeRangedF32, Offset(mMinRotX, GroundCover), &CommonValidators::DegreeRange, MAX_COVERTYPES, "minumum amount of rotation along the X axis to add");
|
|
addFieldV("maxRotX", TypeRangedF32, Offset(mMaxRotX, GroundCover), &CommonValidators::DegreeRange, MAX_COVERTYPES, "maximum amount of rotation along the X axis to add");
|
|
addFieldV("minRotY", TypeRangedF32, Offset(mMinRotY, GroundCover), &CommonValidators::DegreeRange, MAX_COVERTYPES, "minumum amount of rotation along the Y axis to add");
|
|
addFieldV("maxRotY", TypeRangedF32, Offset(mMaxRotY, GroundCover), &CommonValidators::DegreeRange, MAX_COVERTYPES, "maximum amount of rotation along the Y axis to add");
|
|
|
|
addFieldV( "minElevation", TypeRangedF32, Offset( mMinElevation, GroundCover ), &CommonValidators::F32Range, MAX_COVERTYPES, "The minimum world space elevation for placement." );
|
|
|
|
addFieldV( "maxElevation", TypeRangedF32, Offset( mMaxElevation, GroundCover ), &CommonValidators::F32Range, MAX_COVERTYPES, "The maximum world space elevation for placement." );
|
|
|
|
addFieldV( "minClumpCount", TypeRangedS32, Offset( mMinClumpCount, GroundCover ), &CommonValidators::PositiveInt, MAX_COVERTYPES, "The minimum amount of elements in a clump." );
|
|
|
|
addFieldV( "maxClumpCount", TypeRangedS32, Offset( mMaxClumpCount, GroundCover ), &CommonValidators::PositiveInt, MAX_COVERTYPES, "The maximum amount of elements in a clump." );
|
|
|
|
addFieldV( "clumpExponent", TypeRangedF32, Offset( mClumpCountExponent, GroundCover ), &CommonValidators::PositiveFloat, MAX_COVERTYPES, "An exponent used to bias between the minimum and maximum clump counts for a particular clump." );
|
|
|
|
addFieldV( "clumpRadius", TypeRangedF32, Offset( mClumpRadius, GroundCover ), &CommonValidators::PositiveFloat, 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." );
|
|
|
|
addFieldV( "windGustLength", TypeRangedF32, Offset( mWindGustLength, GroundCover ), &CommonValidators::PositiveFloat, "The length in meters between peaks in the wind gust." );
|
|
addFieldV( "windGustFrequency", TypeRangedF32, Offset( mWindGustFrequency, GroundCover ), &CommonValidators::PositiveFloat, "Controls how often the wind gust peaks per second." );
|
|
addFieldV( "windGustStrength", TypeRangedF32, Offset( mWindGustStrength, GroundCover ), &CommonValidators::PositiveFloat, "The maximum distance in meters that the peak wind gust will displace an element." );
|
|
|
|
addFieldV( "windTurbulenceFrequency", TypeRangedF32, Offset( mWindTurbulenceFrequency, GroundCover ), &CommonValidators::PositiveFloat,"Controls the overall rapidity of the wind turbulence." );
|
|
addFieldV( "windTurbulenceStrength", TypeRangedF32, Offset( mWindTurbulenceStrength, GroundCover ), &CommonValidators::PositiveFloat, "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("$pref::GroundCover::fadeScale", TypeF32, &smFadeScale, "A global fade scalar which can reduce the overall rendered distance 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.
|
|
|
|
PACK_ASSET(connection, Material);
|
|
|
|
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( mMinSlope[i] );
|
|
stream->write( mMaxSlope[i] );
|
|
stream->writeFlag(mConformToNormal[i]);
|
|
stream->write(mMinRotX[i]);
|
|
stream->write(mMaxRotX[i]);
|
|
stream->write(mMinRotY[i]);
|
|
stream->write(mMaxRotY[i]);
|
|
|
|
stream->write( mMinElevation[i] );
|
|
stream->write( mMaxElevation[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 );
|
|
}
|
|
|
|
PACK_ASSET_ARRAY_REFACTOR(connection, Layer, MAX_COVERTYPES)
|
|
PACK_ASSET_ARRAY_REFACTOR(connection, Shape, MAX_COVERTYPES)
|
|
|
|
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())
|
|
{
|
|
UNPACK_ASSET(connection, Material);
|
|
|
|
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( &mMinSlope[i] );
|
|
stream->read( &mMaxSlope[i] );
|
|
mConformToNormal[i] = stream->readFlag();
|
|
stream->read(&mMinRotX[i]);
|
|
stream->read(&mMaxRotX[i]);
|
|
stream->read(&mMinRotY[i]);
|
|
stream->read(&mMaxRotY[i]);
|
|
|
|
stream->read( &mMinElevation[i] );
|
|
stream->read( &mMaxElevation[i] );
|
|
|
|
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 );
|
|
}
|
|
|
|
UNPACK_ASSET_ARRAY_REFACTOR(connection, Layer, MAX_COVERTYPES)
|
|
|
|
UNPACK_ASSET_ARRAY_REFACTOR(connection, Shape, MAX_COVERTYPES)
|
|
|
|
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();
|
|
_initShapes();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GroundCover::_initMaterial()
|
|
{
|
|
SAFE_DELETE(mMaterialInst);
|
|
|
|
if (mMaterialAsset.notNull() && mMaterialAsset->getStatus() == MaterialAsset::Ok)
|
|
mMaterialInst = mMaterial->createMatInstance();
|
|
else
|
|
mMaterialInst = 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.
|
|
mMaterialInst->setUserObject( this );
|
|
|
|
// DO IT!
|
|
mMaterialInst->init( features, getGFXVertexFormat<GCVertex>() );
|
|
}
|
|
|
|
void GroundCover::_initShapes()
|
|
{
|
|
_deleteShapes();
|
|
|
|
for ( S32 i=0; i < MAX_COVERTYPES; i++ )
|
|
{
|
|
if ( mShapeAsset[i].isNull() || getShape(i) == NULL)
|
|
continue;
|
|
|
|
if ( isClientObject() && !getShape(i)->preloadMaterialList(getShapeFile(i)) && NetConnection::filesWereDownloaded() )
|
|
{
|
|
Con::warnf( "GroundCover::_initShapes() material preload failed for shape: %s", _getShapeAssetId(i));
|
|
continue;
|
|
}
|
|
|
|
// Create the shape instance.
|
|
mShapeInstances[i] = new TSShapeInstance(getShape(i), 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( mMaterialInst && mMaterialInst->isValid())
|
|
{
|
|
Material* mat = dynamic_cast<Material*>(mMaterialInst->getMaterial());
|
|
if(mat)
|
|
{
|
|
GFXTexHandle tex;
|
|
if (mat->getDiffuseMap(0))
|
|
tex = mat->getDiffuseMap(0);
|
|
|
|
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 bool typeConformToNormal = mConformToNormal[type];
|
|
const F32 typeMinRotX = (mMaxRotX[type] > mMinRotX[type]) ? mMinRotX[type] : mMaxRotX[type];
|
|
const F32 typeMaxRotX = (mMaxRotX[type] > mMinRotX[type]) ? mMaxRotX[type] : mMinRotX[type];
|
|
const F32 typeMinRotY = (mMaxRotY[type] > mMinRotY[type]) ? mMinRotY[type] : mMaxRotY[type];
|
|
const F32 typeMaxRotY = (mMaxRotY[type] > mMinRotY[type]) ? mMaxRotY[type] : mMinRotY[type];
|
|
|
|
const F32 typeSizeRange = mSizeMax[type] - mSizeMin[type];
|
|
const F32 typeMinSlope = mMinSlope[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()->mBounds : Box3F();
|
|
const F32 typeWindScale = mWindScale[type];
|
|
|
|
StringTableEntry typeLayer = StringTable->EmptyString();
|
|
if (mLayerAsset[type].notNull())
|
|
typeLayer = mLayerAsset[type]->getAssetId();
|
|
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 blockIDx = 0; blockIDx < terrainBlocks.size(); blockIDx++ )
|
|
{
|
|
TerrainBlock *terrain = dynamic_cast< TerrainBlock* >( terrainBlocks[ blockIDx ] );
|
|
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 );
|
|
// Transform billboard point into terrain's frame of reference.
|
|
Point3F pp = Point3F(cp.x, cp.y, 0);
|
|
terrainBlock->getWorldTransform().mulP(pp);
|
|
hit = terrainBlock->getNormalHeightMaterial( Point2F ( pp.x, pp.y ),
|
|
&normal, &h, matName );
|
|
PROFILE_END(); // GroundCover_TerrainRayCast
|
|
|
|
// TODO: When did we loose the world space elevation when
|
|
// getting the terrain height?
|
|
h += pos.z + mZOffset;
|
|
|
|
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;
|
|
}
|
|
|
|
if (!mIsZero(typeMinSlope))
|
|
{
|
|
if (mAcos(normal.z) < mDegToRad(typeMinSlope))
|
|
continue;
|
|
}
|
|
point.set( cp.x, cp.y, h );
|
|
p.point = point;
|
|
p.rotation = rotation;
|
|
p.normal = normal;
|
|
if (!typeConformToNormal)
|
|
{
|
|
p.normal.y = 0;
|
|
p.normal.x = 0;
|
|
}
|
|
p.normal.x += rand.randF(typeMinRotX, typeMaxRotX);
|
|
p.normal.y += rand.randF(typeMinRotY, typeMaxRotY);
|
|
// 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 (mMaterialInst == NULL)
|
|
return;
|
|
|
|
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->getCullingFrustum();
|
|
|
|
// 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() && mMaterialInst->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.
|
|
F32 radius = mRadius * smFadeScale;
|
|
mCuller.setFarDist(radius * 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 * smFadeScale * cullScale * screenScale, radius * cullScale * screenScale );
|
|
|
|
const F32 simTime = Sim::getCurrentTime() * 0.001f;
|
|
|
|
mShaderConstData.gustInfo.set( mWindGustLength, mWindGustFrequency * simTime, mWindGustStrength );
|
|
mShaderConstData.turbInfo.set( mWindTurbulenceFrequency * simTime, mWindTurbulenceStrength );
|
|
|
|
// Use the camera's forward vector to calculate the camera's right
|
|
// and up vectors. This removes any camera banking from affecting
|
|
// the ground cover.
|
|
const MatrixF &camMat = state->getDiffuseCameraTransform();
|
|
Point3F camDir, camUp, camRight;
|
|
camMat.getColumn( 1, &camDir );
|
|
mCross( camDir, Point3F::UnitZ, &camRight );
|
|
if ( camRight.magnitudeSafe() == 0.0f )
|
|
{
|
|
camRight.set( 0.0f, -1.0f, 0.0f );
|
|
}
|
|
camRight.normalizeSafe();
|
|
mCross( camRight, camDir, &camUp );
|
|
|
|
// 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, mMaterialInst, &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*smFadeScale);
|
|
|
|
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 ) );
|
|
}
|
|
}
|