mirror of
https://github.com/TorqueGameEngines/Torque3D.git
synced 2026-01-20 04:34:48 +00:00
1760 lines
61 KiB
C++
1760 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;
|
|
|
|
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 );
|
|
|
|
INIT_ASSET_ARRAY(Shape, i);
|
|
|
|
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." );
|
|
|
|
addField("shapeFilename", TypeFilename, Offset(mShapeName, GroundCover), MAX_COVERTYPES, "The cover shape filename. [Optional]", AbstractClassRep::FIELD_HideInInspectors);
|
|
INITPERSISTFIELD_SHAPEASSET_ARRAY(Shape, MAX_COVERTYPES, GroundCover, "The cover shape. [Optional]");
|
|
|
|
addField( "layer", TypeTerrainMaterialAssetId, Offset( mLayer, GroundCover ), MAX_COVERTYPES, "Terrain material assetId 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." );
|
|
|
|
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->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 );
|
|
|
|
PACK_ASSET_ARRAY(connection, Shape, 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())
|
|
{
|
|
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] );
|
|
|
|
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 );
|
|
|
|
UNPACK_ASSET_ARRAY(connection, Shape, i);
|
|
}
|
|
|
|
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(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() || mShape[i] == nullptr)
|
|
continue;
|
|
|
|
if ( isClientObject() && !mShape[i]->preloadMaterialList(mShape[i].getPath()) && NetConnection::filesWereDownloaded() )
|
|
{
|
|
Con::warnf( "GroundCover::_initShapes() material preload failed for shape: %s", mShapeAssetId[i] );
|
|
continue;
|
|
}
|
|
|
|
// Create the shape instance.
|
|
mShapeInstances[i] = new TSShapeInstance(mShape[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->getDiffuseMapResource(0))
|
|
tex = mat->getDiffuseMapResource(0);
|
|
else if (mat->getDiffuseMap(0) != StringTable->EmptyString())
|
|
tex = GFXTexHandle(mat->getDiffuseMap(0), &GFXStaticTextureSRGBProfile, "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 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 = 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 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 == nullptr)
|
|
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 ) );
|
|
}
|
|
}
|