Torque3D/Engine/source/terrain/terrData.cpp
2013-06-13 10:31:45 +10:00

1374 lines
39 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 "terrain/terrData.h"
#include "terrain/terrCollision.h"
#include "terrain/terrCell.h"
#include "terrain/terrRender.h"
#include "terrain/terrMaterial.h"
#include "terrain/terrCellMaterial.h"
#include "gui/worldEditor/terrainEditor.h"
#include "math/mathIO.h"
#include "core/stream/fileStream.h"
#include "core/stream/bitStream.h"
#include "console/consoleTypes.h"
#include "sim/netConnection.h"
#include "core/util/safeDelete.h"
#include "T3D/objectTypes.h"
#include "renderInstance/renderPassManager.h"
#include "scene/sceneRenderState.h"
#include "materials/materialManager.h"
#include "materials/baseMatInstance.h"
#include "gfx/gfxTextureManager.h"
#include "gfx/gfxCardProfile.h"
#include "core/resourceManager.h"
#include "T3D/physics/physicsPlugin.h"
#include "T3D/physics/physicsBody.h"
#include "T3D/physics/physicsCollision.h"
#include "console/engineAPI.h"
using namespace Torque;
IMPLEMENT_CO_NETOBJECT_V1(TerrainBlock);
ConsoleDocClass( TerrainBlock,
"@brief Represent a terrain object in a Torque 3D level\n\n"
"@tsexample\n"
"new TerrainBlock(theTerrain)\n"
"{\n"
" terrainFile = \"art/terrains/Deathball Desert_0.ter\";\n"
" squareSize = \"2\";\n"
" tile = \"0\";\n"
" baseTexSize = \"1024\";\n"
" screenError = \"16\";\n"
" position = \"-1024 -1024 179.978\";\n"
" rotation = \"1 0 0 0\";\n"
" scale = \"1 1 1\";\n"
" isRenderEnabled = \"true\";\n"
" canSaveDynamicFields = \"1\";\n"
"};\n"
"@endtsexample\n\n"
"@see TerrainMaterial\n\n"
"@ingroup Terrain\n"
);
Signal<void(U32,TerrainBlock*,const Point2I& ,const Point2I&)> TerrainBlock::smUpdateSignal;
F32 TerrainBlock::smLODScale = 1.0f;
F32 TerrainBlock::smDetailScale = 1.0f;
//RBP - Global function declared in Terrdata.h
TerrainBlock* getTerrainUnderWorldPoint(const Point3F & wPos)
{
// Cast a ray straight down from the world position and see which
// Terrain is the closest to our starting point
Point3F startPnt = wPos;
Point3F endPnt = wPos + Point3F(0.0f, 0.0f, -10000.0f);
S32 blockIndex = -1;
F32 nearT = 1.0f;
SimpleQueryList queryList;
gServerContainer.findObjects( TerrainObjectType, SimpleQueryList::insertionCallback, &queryList);
for (U32 i = 0; i < queryList.mList.size(); i++)
{
Point3F tStartPnt, tEndPnt;
TerrainBlock* terrBlock = dynamic_cast<TerrainBlock*>(queryList.mList[i]);
terrBlock->getWorldTransform().mulP(startPnt, &tStartPnt);
terrBlock->getWorldTransform().mulP(endPnt, &tEndPnt);
RayInfo ri;
if (terrBlock->castRayI(tStartPnt, tEndPnt, &ri, true))
{
if (ri.t < nearT)
{
blockIndex = i;
nearT = ri.t;
}
}
}
if (blockIndex > -1)
return (TerrainBlock*)(queryList.mList[blockIndex]);
return NULL;
}
ConsoleDocFragment _getTerrainUnderWorldPoint1(
"@brief Gets the terrain block that is located under the given world point\n\n"
"@param position The world space coordinate you wish to query at. Formatted as (\"x y z\")\n\n"
"@return Returns the ID of the requested terrain block (0 if not found).\n\n"
"@ingroup Terrain",
NULL,
"bool getTerrainUnderWorldPoint( Point3F position );"
);
ConsoleDocFragment _getTerrainUnderWorldPoint2(
"@brief Takes a world point and find the \"highest\" terrain underneath it\n\n"
"@param x The X coordinate in world space\n"
"@param y The Y coordinate in world space\n\n"
"@param z The Z coordinate in world space\n\n"
"@return Returns the ID of the requested terrain block (0 if not found).\n\n"
"@ingroup Terrain",
NULL,
"bool getTerrainUnderWorldPoint( F32 x, F32 y, F32 z);"
);
ConsoleFunction(getTerrainUnderWorldPoint, S32, 2, 4, "(Point3F x/y/z) Gets the terrain block that is located under the given world point.\n"
"@param x/y/z The world coordinates (floating point values) you wish to query at. "
"These can be formatted as either a string (\"x y z\") or separately as (x, y, z)\n"
"@return Returns the ID of the requested terrain block (0 if not found).\n\n"
"@hide")
{
Point3F pos;
if(argc == 2)
dSscanf(argv[1], "%f %f %f", &pos.x, &pos.y, &pos.z);
else if(argc == 4)
{
pos.x = dAtof(argv[1]);
pos.y = dAtof(argv[2]);
pos.z = dAtof(argv[3]);
}
else
{
Con::errorf("getTerrainUnderWorldPoint(Point3F): Invalid argument count! Valid arguments are either \"x y z\" or x,y,z\n");
return 0;
}
TerrainBlock* terrain = getTerrainUnderWorldPoint(pos);
if(terrain != NULL)
{
return terrain->getId();
}
return 0;
}
TerrainBlock::TerrainBlock()
: mSquareSize( 1.0f ),
mCastShadows( true ),
mScreenError( 16 ),
mDetailsDirty( false ),
mLayerTexDirty( false ),
mLightMap( NULL ),
mLightMapSize( 256 ),
mMaxDetailDistance( 0.0f ),
mCell( NULL ),
mCRC( 0 ),
mBaseTexSize( 1024 ),
mBaseMaterial( NULL ),
mDefaultMatInst( NULL ),
mBaseTexScaleConst( NULL ),
mBaseTexIdConst( NULL ),
mPhysicsRep( NULL ),
mZoningDirty( false )
{
mTypeMask = TerrainObjectType | StaticObjectType | StaticShapeObjectType;
mNetFlags.set(Ghostable | ScopeAlways);
}
extern Convex sTerrainConvexList;
TerrainBlock::~TerrainBlock()
{
// Kill collision
sTerrainConvexList.nukeList();
SAFE_DELETE(mLightMap);
mLightMapTex = NULL;
#ifdef TORQUE_TOOLS
TerrainEditor* editor = dynamic_cast<TerrainEditor*>(Sim::findObject("ETerrainEditor"));
if (editor)
editor->detachTerrain(this);
#endif
}
void TerrainBlock::_onTextureEvent( GFXTexCallbackCode code )
{
if ( code == GFXZombify )
{
if ( mBaseTex.isValid() &&
mBaseTex->isRenderTarget() )
mBaseTex = NULL;
mLayerTex = NULL;
mLightMapTex = NULL;
}
}
bool TerrainBlock::_setSquareSize( void *obj, const char *index, const char *data )
{
TerrainBlock *terrain = static_cast<TerrainBlock*>( obj );
F32 newSqaureSize = dAtof( data );
if ( !mIsEqual( terrain->mSquareSize, newSqaureSize ) )
{
terrain->mSquareSize = newSqaureSize;
if ( terrain->isServerObject() && terrain->isProperlyAdded() )
terrain->_updateBounds();
terrain->setMaskBits( HeightMapChangeMask | SizeMask );
}
return false;
}
bool TerrainBlock::_setBaseTexSize( void *obj, const char *index, const char *data )
{
TerrainBlock *terrain = static_cast<TerrainBlock*>( obj );
// NOTE: We're limiting the base texture size to
// 2048 as anything greater in size becomes too
// large to generate for many cards.
//
// If you want to remove this limit feel free, but
// prepare for problems if you don't ship the baked
// base texture with your installer.
//
S32 texSize = mClamp( dAtoi( data ), 0, 2048 );
if ( terrain->mBaseTexSize != texSize )
{
terrain->mBaseTexSize = texSize;
terrain->setMaskBits( MaterialMask );
}
return false;
}
bool TerrainBlock::_setLightMapSize( void *obj, const char *index, const char *data )
{
TerrainBlock *terrain = static_cast<TerrainBlock*>(obj);
// Handle inspector value decrements correctly
U32 mapSize = dAtoi( data );
if ( mapSize == terrain->mLightMapSize-1 )
mapSize = terrain->mLightMapSize/2;
// Limit the lightmap size, and ensure it is a power of 2
const U32 maxTextureSize = GFX->getCardProfiler()->queryProfile( "maxTextureSize", 1024 );
mapSize = mClamp( getNextPow2( mapSize ), 0, maxTextureSize );
if ( terrain->mLightMapSize != mapSize )
{
terrain->mLightMapSize = mapSize;
terrain->setMaskBits( MaterialMask );
}
return false;
}
bool TerrainBlock::setFile( const FileName &terrFileName )
{
if ( terrFileName == mTerrFileName )
return mFile != NULL;
Resource<TerrainFile> file = ResourceManager::get().load( terrFileName );
if( !file )
return false;
setFile( file );
setMaskBits( FileMask | HeightMapChangeMask );
return true;
}
void TerrainBlock::setFile( Resource<TerrainFile> terr )
{
mFile = terr;
mTerrFileName = terr.getPath();
}
bool TerrainBlock::save(const char *filename)
{
return mFile->save(filename);
}
bool TerrainBlock::_setTerrainFile( void *obj, const char *index, const char *data )
{
static_cast<TerrainBlock*>( obj )->setFile( FileName( data ) );
return false;
}
void TerrainBlock::_updateBounds()
{
if ( !mFile )
return; // quick fix to stop crashing when deleting terrainblocks
// Setup our object space bounds.
mBounds.minExtents.set( 0.0f, 0.0f, 0.0f );
mBounds.maxExtents.set( getWorldBlockSize(), getWorldBlockSize(), 0.0f );
getMinMaxHeight( &mBounds.minExtents.z, &mBounds.maxExtents.z );
// Set our mObjBox to be equal to mBounds
if ( mObjBox.maxExtents != mBounds.maxExtents ||
mObjBox.minExtents != mBounds.minExtents )
{
mObjBox = mBounds;
resetWorldBox();
}
}
void TerrainBlock::_onZoningChanged( SceneZoneSpaceManager *zoneManager )
{
if ( mCell == NULL || zoneManager != getSceneManager()->getZoneManager() )
return;
mZoningDirty = true;
}
void TerrainBlock::setHeight( const Point2I &pos, F32 height )
{
U16 ht = floatToFixed( height );
mFile->setHeight( pos.x, pos.y, ht );
// Note: We do not update the grid here as this could
// be called several times in a loop. We depend on the
// caller doing a grid update when he is done.
}
F32 TerrainBlock::getHeight( const Point2I &pos )
{
U16 ht = mFile->getHeight( pos.x, pos.y );
return fixedToFloat( ht );
}
void TerrainBlock::updateGridMaterials( const Point2I &minPt, const Point2I &maxPt )
{
if ( mCell )
{
// Tell the terrain cell that something changed.
const RectI gridRect( minPt, maxPt - minPt );
mCell->updateGrid( gridRect, true );
}
// We mark us as dirty... it will be updated
// before the next time we render the terrain.
mLayerTexDirty = true;
// Signal anyone that cares that the opacity was changed.
smUpdateSignal.trigger( LayersUpdate, this, minPt, maxPt );
}
Point2I TerrainBlock::getGridPos( const Point3F &worldPos ) const
{
Point3F terrainPos = worldPos;
getWorldTransform().mulP( terrainPos );
F32 squareSize = ( F32 ) getSquareSize();
F32 halfSquareSize = squareSize / 2.0;
F32 x = ( terrainPos.x + halfSquareSize ) / squareSize;
F32 y = ( terrainPos.y + halfSquareSize ) / squareSize;
Point2I gridPos( ( S32 ) mFloor( x ), ( S32 ) mFloor( y ) );
return gridPos;
}
void TerrainBlock::updateGrid( const Point2I &minPt, const Point2I &maxPt, bool updateClient )
{
// On the client we just signal everyone that the height
// map has changed... the server does the actual changes.
if ( isClientObject() )
{
PROFILE_SCOPE( TerrainBlock_updateGrid_Client );
// This depends on the client getting this call 'after' the server.
// Which is currently the case.
_updateBounds();
mZoningDirty = true;
smUpdateSignal.trigger( HeightmapUpdate, this, minPt, maxPt );
// Tell the terrain cell that the height changed.
const RectI gridRect( minPt, maxPt - minPt );
mCell->updateGrid( gridRect );
// Rebuild the physics representation.
if ( mPhysicsRep )
{
// Delay the update by a few milliseconds so
// that we're not rebuilding during an active
// editing operation.
mPhysicsRep->queueCallback( 500, Delegate<void()>( this, &TerrainBlock::_updatePhysics ) );
}
return;
}
// Now on the server we rebuild the
// affected area of the grid map.
mFile->updateGrid( minPt, maxPt );
// Fix up the bounds.
_updateBounds();
// Rebuild the physics representation.
if ( mPhysicsRep )
{
// Delay the update by a few milliseconds so
// that we're not rebuilding during an active
// editing operation.
mPhysicsRep->queueCallback( 500, Delegate<void()>( this, &TerrainBlock::_updatePhysics ) );
}
// Signal again here for any server side observers.
smUpdateSignal.trigger( HeightmapUpdate, this, minPt, maxPt );
// If this is a server object and the client update
// was requested then try to use the local connection
// pointer to do it.
if ( updateClient && getClientObject() )
((TerrainBlock*)getClientObject())->updateGrid( minPt, maxPt, false );
}
bool TerrainBlock::getHeight( const Point2F &pos, F32 *height ) const
{
PROFILE_SCOPE( TerrainBlock_getHeight );
F32 invSquareSize = 1.0f / mSquareSize;
F32 xp = pos.x * invSquareSize;
F32 yp = pos.y * invSquareSize;
S32 x = S32(xp);
S32 y = S32(yp);
xp -= (F32)x;
yp -= (F32)y;
const U32 blockMask = mFile->mSize - 1;
if ( x & ~blockMask || y & ~blockMask )
return false;
x &= blockMask;
y &= blockMask;
const TerrainSquare *sq = mFile->findSquare( 0, x, y );
if ( sq->flags & TerrainSquare::Empty )
return false;
F32 zBottomLeft = fixedToFloat( mFile->getHeight( x, y ) );
F32 zBottomRight = fixedToFloat( mFile->getHeight( x + 1, y ) );
F32 zTopLeft = fixedToFloat( mFile->getHeight( x, y + 1 ) );
F32 zTopRight = fixedToFloat( mFile->getHeight( x + 1, y + 1 ) );
if ( sq->flags & TerrainSquare::Split45 )
{
if (xp>yp)
// bottom half
*height = zBottomLeft + xp * (zBottomRight-zBottomLeft) + yp * (zTopRight-zBottomRight);
else
// top half
*height = zBottomLeft + xp * (zTopRight-zTopLeft) + yp * (zTopLeft-zBottomLeft);
}
else
{
if (1.0f-xp>yp)
// bottom half
*height = zBottomRight + (1.0f-xp) * (zBottomLeft-zBottomRight) + yp * (zTopLeft-zBottomLeft);
else
// top half
*height = zBottomRight + (1.0f-xp) * (zTopLeft-zTopRight) + yp * (zTopRight-zBottomRight);
}
return true;
}
bool TerrainBlock::getNormal( const Point2F &pos, Point3F *normal, bool normalize, bool skipEmpty ) const
{
PROFILE_SCOPE( TerrainBlock_getNormal );
F32 invSquareSize = 1.0f / mSquareSize;
F32 xp = pos.x * invSquareSize;
F32 yp = pos.y * invSquareSize;
S32 x = S32(xp);
S32 y = S32(yp);
xp -= (F32)x;
yp -= (F32)y;
const U32 blockMask = mFile->mSize - 1;
if ( x & ~blockMask || y & ~blockMask )
return false;
x &= blockMask;
y &= blockMask;
const TerrainSquare *sq = mFile->findSquare( 0, x, y );
if ( skipEmpty && sq->flags & TerrainSquare::Empty )
return false;
F32 zBottomLeft = fixedToFloat( mFile->getHeight( x, y ) );
F32 zBottomRight = fixedToFloat( mFile->getHeight( x + 1, y ) );
F32 zTopLeft = fixedToFloat( mFile->getHeight( x, y + 1 ) );
F32 zTopRight = fixedToFloat( mFile->getHeight( x + 1, y + 1 ) );
if ( sq->flags & TerrainSquare::Split45 )
{
if (xp>yp)
// bottom half
normal->set(zBottomLeft-zBottomRight, zBottomRight-zTopRight, mSquareSize);
else
// top half
normal->set(zTopLeft-zTopRight, zBottomLeft-zTopLeft, mSquareSize);
}
else
{
if (1.0f-xp>yp)
// bottom half
normal->set(zBottomLeft-zBottomRight, zBottomLeft-zTopLeft, mSquareSize);
else
// top half
normal->set(zTopLeft-zTopRight, zBottomRight-zTopRight, mSquareSize);
}
if (normalize)
normal->normalize();
return true;
}
bool TerrainBlock::getSmoothNormal( const Point2F &pos,
Point3F *normal,
bool normalize,
bool skipEmpty ) const
{
PROFILE_SCOPE( TerrainBlock_getSmoothNormal );
F32 invSquareSize = 1.0f / mSquareSize;
F32 xp = pos.x * invSquareSize;
F32 yp = pos.y * invSquareSize;
S32 x = S32(xp);
S32 y = S32(yp);
const U32 blockMask = mFile->mSize - 1;
if ( x & ~blockMask || y & ~blockMask )
return false;
x &= blockMask;
y &= blockMask;
const TerrainSquare *sq = mFile->findSquare( 0, x, y );
if ( skipEmpty && sq->flags & TerrainSquare::Empty )
return false;
F32 h1 = fixedToFloat( mFile->getHeight( x + 1, y ) );
F32 h2 = fixedToFloat( mFile->getHeight( x, y + 1 ) );
F32 h3 = fixedToFloat( mFile->getHeight( x - 1, y ) );
F32 h4 = fixedToFloat( mFile->getHeight( x, y - 1 ) );
normal->set( h3 - h1, h4 - h2, mSquareSize * 2.0f );
if ( normalize )
normal->normalize();
return true;
}
bool TerrainBlock::getNormalAndHeight( const Point2F &pos, Point3F *normal, F32 *height, bool normalize ) const
{
PROFILE_SCOPE( TerrainBlock_getNormalAndHeight );
F32 invSquareSize = 1.0f / mSquareSize;
F32 xp = pos.x * invSquareSize;
F32 yp = pos.y * invSquareSize;
S32 x = S32(xp);
S32 y = S32(yp);
xp -= (F32)x;
yp -= (F32)y;
const U32 blockMask = mFile->mSize - 1;
if ( x & ~blockMask || y & ~blockMask )
return false;
x &= blockMask;
y &= blockMask;
const TerrainSquare *sq = mFile->findSquare( 0, x, y );
if ( sq->flags & TerrainSquare::Empty )
return false;
F32 zBottomLeft = fixedToFloat( mFile->getHeight(x, y) );
F32 zBottomRight = fixedToFloat( mFile->getHeight(x + 1, y) );
F32 zTopLeft = fixedToFloat( mFile->getHeight(x, y + 1) );
F32 zTopRight = fixedToFloat( mFile->getHeight(x + 1, y + 1) );
if ( sq->flags & TerrainSquare::Split45 )
{
if (xp>yp)
{
// bottom half
normal->set(zBottomLeft-zBottomRight, zBottomRight-zTopRight, mSquareSize);
*height = zBottomLeft + xp * (zBottomRight-zBottomLeft) + yp * (zTopRight-zBottomRight);
}
else
{
// top half
normal->set(zTopLeft-zTopRight, zBottomLeft-zTopLeft, mSquareSize);
*height = zBottomLeft + xp * (zTopRight-zTopLeft) + yp * (zTopLeft-zBottomLeft);
}
}
else
{
if (1.0f-xp>yp)
{
// bottom half
normal->set(zBottomLeft-zBottomRight, zBottomLeft-zTopLeft, mSquareSize);
*height = zBottomRight + (1.0f-xp) * (zBottomLeft-zBottomRight) + yp * (zTopLeft-zBottomLeft);
}
else
{
// top half
normal->set(zTopLeft-zTopRight, zBottomRight-zTopRight, mSquareSize);
*height = zBottomRight + (1.0f-xp) * (zTopLeft-zTopRight) + yp * (zTopRight-zBottomRight);
}
}
if (normalize)
normal->normalize();
return true;
}
bool TerrainBlock::getNormalHeightMaterial( const Point2F &pos,
Point3F *normal,
F32 *height,
StringTableEntry &matName ) const
{
PROFILE_SCOPE( TerrainBlock_getNormalHeightMaterial );
F32 invSquareSize = 1.0f / mSquareSize;
F32 xp = pos.x * invSquareSize;
F32 yp = pos.y * invSquareSize;
S32 x = S32(xp);
S32 y = S32(yp);
S32 xm = S32(mFloor( xp + 0.5f ));
S32 ym = S32(mFloor( yp + 0.5f ));
xp -= (F32)x;
yp -= (F32)y;
const U32 blockMask = mFile->mSize - 1;
if ( x & ~blockMask || y & ~blockMask )
return false;
x &= blockMask;
y &= blockMask;
const TerrainSquare *sq = mFile->findSquare( 0, x, y );
if ( sq->flags & TerrainSquare::Empty )
return false;
F32 zBottomLeft = fixedToFloat( mFile->getHeight(x, y) );
F32 zBottomRight = fixedToFloat( mFile->getHeight(x + 1, y) );
F32 zTopLeft = fixedToFloat( mFile->getHeight(x, y + 1) );
F32 zTopRight = fixedToFloat( mFile->getHeight(x + 1, y + 1) );
matName = mFile->getMaterialName( xm, ym );
if ( sq->flags & TerrainSquare::Split45 )
{
if (xp>yp)
{
// bottom half
normal->set(zBottomLeft-zBottomRight, zBottomRight-zTopRight, mSquareSize);
*height = zBottomLeft + xp * (zBottomRight-zBottomLeft) + yp * (zTopRight-zBottomRight);
}
else
{
// top half
normal->set(zTopLeft-zTopRight, zBottomLeft-zTopLeft, mSquareSize);
*height = zBottomLeft + xp * (zTopRight-zTopLeft) + yp * (zTopLeft-zBottomLeft);
}
}
else
{
if (1.0f-xp>yp)
{
// bottom half
normal->set(zBottomLeft-zBottomRight, zBottomLeft-zTopLeft, mSquareSize);
*height = zBottomRight + (1.0f-xp) * (zBottomLeft-zBottomRight) + yp * (zTopLeft-zBottomLeft);
}
else
{
// top half
normal->set(zTopLeft-zTopRight, zBottomRight-zTopRight, mSquareSize);
*height = zBottomRight + (1.0f-xp) * (zTopLeft-zTopRight) + yp * (zTopRight-zBottomRight);
}
}
normal->normalize();
return true;
}
U32 TerrainBlock::getMaterialCount() const
{
return mFile->mMaterials.size();
}
void TerrainBlock::addMaterial( const String &name, U32 insertAt )
{
TerrainMaterial *mat = TerrainMaterial::findOrCreate( name );
if ( insertAt == -1 )
{
mFile->mMaterials.push_back( mat );
mFile->_initMaterialInstMapping();
}
else
{
// TODO: Insert and reindex!
}
mDetailsDirty = true;
mLayerTexDirty = true;
}
void TerrainBlock::removeMaterial( U32 index )
{
// Cannot delete if only one layer.
if ( mFile->mMaterials.size() == 1 )
return;
mFile->mMaterials.erase( index );
mFile->_initMaterialInstMapping();
for ( S32 i = 0; i < mFile->mLayerMap.size(); i++ )
{
if ( mFile->mLayerMap[i] >= index &&
mFile->mLayerMap[i] != 0 )
{
mFile->mLayerMap[i]--;
}
}
mDetailsDirty = true;
mLayerTexDirty = true;
}
void TerrainBlock::updateMaterial( U32 index, const String &name )
{
if ( index >= mFile->mMaterials.size() )
return;
mFile->mMaterials[ index ] = TerrainMaterial::findOrCreate( name );
mFile->_initMaterialInstMapping();
mDetailsDirty = true;
mLayerTexDirty = true;
}
TerrainMaterial* TerrainBlock::getMaterial( U32 index ) const
{
if ( index >= mFile->mMaterials.size() )
return NULL;
return mFile->mMaterials[ index ];
}
void TerrainBlock::deleteAllMaterials()
{
mFile->mMaterials.clear();
mFile->mMaterialInstMapping.clearMatInstList();
}
const char* TerrainBlock::getMaterialName( U32 index ) const
{
if ( index < mFile->mMaterials.size() )
return mFile->mMaterials[ index ]->getInternalName();
return NULL;
}
void TerrainBlock::setLightMap( GBitmap *newLightMap )
{
SAFE_DELETE( mLightMap );
mLightMap = newLightMap;
mLightMapTex = NULL;
}
void TerrainBlock::clearLightMap()
{
if ( !mLightMap )
mLightMap = new GBitmap( mLightMapSize, mLightMapSize, 0, GFXFormatR8G8B8 );
mLightMap->fillWhite();
mLightMapTex = NULL;
}
GFXTextureObject* TerrainBlock::getLightMapTex()
{
if ( mLightMapTex.isNull() && mLightMap )
{
mLightMapTex.set( mLightMap,
&GFXDefaultStaticDiffuseProfile,
false,
"TerrainBlock::getLightMapTex()" );
}
return mLightMapTex;
}
void TerrainBlock::onEditorEnable()
{
}
void TerrainBlock::onEditorDisable()
{
}
bool TerrainBlock::onAdd()
{
if(!Parent::onAdd())
return false;
if ( mTerrFileName.isEmpty() )
{
mTerrFileName = Con::getVariable( "$Client::MissionFile" );
String terrainDirectory( Con::getVariable( "$pref::Directories::Terrain" ) );
if ( terrainDirectory.isEmpty() )
{
terrainDirectory = "art/terrains/";
}
mTerrFileName.replace("tools/levels/", terrainDirectory);
mTerrFileName.replace("levels/", terrainDirectory);
Vector<String> materials;
materials.push_back( "warning_material" );
TerrainFile::create( &mTerrFileName, 256, materials );
}
Resource<TerrainFile> terr = ResourceManager::get().load( mTerrFileName );
if(terr == NULL)
{
if(isClientObject())
NetConnection::setLastError("You are missing a file needed to play this mission: %s", mTerrFileName.c_str());
return false;
}
setFile( terr );
if ( terr->mNeedsResaving )
{
if (Platform::messageBox("Update Terrain File", "You appear to have a Terrain file in an older format. Do you want Torque to update it?", MBOkCancel, MIQuestion) == MROk)
{
terr->save(terr->mFilePath.getFullPath());
terr->mNeedsResaving = false;
}
}
if (terr->mFileVersion != TerrainFile::FILE_VERSION || terr->mNeedsResaving)
{
Con::errorf(" *********************************************************");
Con::errorf(" *********************************************************");
Con::errorf(" *********************************************************");
Con::errorf(" PLEASE RESAVE THE TERRAIN FILE FOR THIS MISSION! THANKS!");
Con::errorf(" *********************************************************");
Con::errorf(" *********************************************************");
Con::errorf(" *********************************************************");
}
_updateBounds();
resetWorldBox();
setRenderTransform(mObjToWorld);
if (isClientObject())
{
if ( mCRC != terr.getChecksum() )
{
NetConnection::setLastError("Your terrain file doesn't match the version that is running on the server.");
return false;
}
clearLightMap();
// Init the detail layer rendering helper.
_updateMaterials();
_updateLayerTexture();
// If the cached base texture is older that the terrain file or
// it doesn't exist then generate and cache it.
String baseCachePath = _getBaseTexCacheFileName();
if ( Platform::compareModifiedTimes( baseCachePath, mTerrFileName ) < 0 )
_updateBaseTexture( true );
// The base texture should have been cached by now... so load it.
mBaseTex.set( baseCachePath, &GFXDefaultStaticDiffuseProfile, "TerrainBlock::mBaseTex" );
GFXTextureManager::addEventDelegate( this, &TerrainBlock::_onTextureEvent );
MATMGR->getFlushSignal().notify( this, &TerrainBlock::_onFlushMaterials );
// Build the terrain quadtree.
_rebuildQuadtree();
// Preload all the materials.
mCell->preloadMaterials();
mZoningDirty = true;
SceneZoneSpaceManager::getZoningChangedSignal().notify( this, &TerrainBlock::_onZoningChanged );
}
else
mCRC = terr.getChecksum();
addToScene();
_updatePhysics();
return true;
}
String TerrainBlock::_getBaseTexCacheFileName() const
{
Torque::Path basePath( mTerrFileName );
basePath.setFileName( basePath.getFileName() + "_basetex" );
basePath.setExtension( "dds" );
return basePath.getFullPath();
}
void TerrainBlock::_rebuildQuadtree()
{
SAFE_DELETE( mCell );
// Recursively build the cells.
mCell = TerrCell::init( this );
// Build the shared PrimitiveBuffer.
mCell->createPrimBuffer( &mPrimBuffer );
}
void TerrainBlock::_updatePhysics()
{
if ( !PHYSICSMGR )
return;
SAFE_DELETE( mPhysicsRep );
PhysicsCollision *colShape;
// If we can steal the collision shape from the local server
// object then do so as it saves us alot of cpu time and memory.
//
// TODO: We should move this sharing down into TerrFile where
// it probably belongs.
//
if ( getServerObject() )
{
TerrainBlock *serverTerrain = (TerrainBlock*)getServerObject();
colShape = serverTerrain->mPhysicsRep->getColShape();
}
else
{
// Get empty state of each vert
bool *holes = new bool[ getBlockSize() * getBlockSize() ];
for ( U32 row = 0; row < getBlockSize(); row++ )
for ( U32 column = 0; column < getBlockSize(); column++ )
holes[ row + (column * getBlockSize()) ] = mFile->isEmptyAt( row, column );
colShape = PHYSICSMGR->createCollision();
colShape->addHeightfield( mFile->getHeightMap().address(), holes, getBlockSize(), mSquareSize, MatrixF::Identity );
delete [] holes;
}
PhysicsWorld *world = PHYSICSMGR->getWorld( isServerObject() ? "server" : "client" );
mPhysicsRep = PHYSICSMGR->createBody();
mPhysicsRep->init( colShape, 0, 0, this, world );
mPhysicsRep->setTransform( getTransform() );
}
void TerrainBlock::onRemove()
{
removeFromScene();
SceneZoneSpaceManager::getZoningChangedSignal().remove( this, &TerrainBlock::_onZoningChanged );
SAFE_DELETE( mPhysicsRep );
if ( isClientObject() )
{
mBaseTex = NULL;
mLayerTex = NULL;
SAFE_DELETE( mBaseMaterial );
SAFE_DELETE( mDefaultMatInst );
SAFE_DELETE( mCell );
mPrimBuffer = NULL;
mBaseShader = NULL;
GFXTextureManager::removeEventDelegate( this, &TerrainBlock::_onTextureEvent );
MATMGR->getFlushSignal().remove( this, &TerrainBlock::_onFlushMaterials );
}
Parent::onRemove();
}
void TerrainBlock::prepRenderImage( SceneRenderState* state )
{
PROFILE_SCOPE(TerrainBlock_prepRenderImage);
// If we need to update our cached
// zone state then do it now.
if ( mZoningDirty )
{
mZoningDirty = false;
mCell->updateZoning( getSceneManager()->getZoneManager() );
}
_renderBlock( state );
}
void TerrainBlock::setTransform(const MatrixF & mat)
{
Parent::setTransform( mat );
// Update world-space OBBs.
if( mCell )
{
mCell->updateOBBs();
mZoningDirty = true;
}
if ( mPhysicsRep )
mPhysicsRep->setTransform( mat );
setRenderTransform( mat );
setMaskBits( TransformMask );
if(isClientObject())
smUpdateSignal.trigger( HeightmapUpdate, this, Point2I::Zero, Point2I::Max );
}
void TerrainBlock::setScale( const VectorF &scale )
{
// We disable scaling... we never scale!
Parent::setScale( VectorF::One );
}
void TerrainBlock::initPersistFields()
{
addGroup( "Media" );
addProtectedField( "terrainFile", TypeStringFilename, Offset( mTerrFileName, TerrainBlock ),
&TerrainBlock::_setTerrainFile, &defaultProtectedGetFn,
"The source terrain data file." );
endGroup( "Media" );
addGroup( "Misc" );
addField( "castShadows", TypeBool, Offset( mCastShadows, TerrainBlock ),
"Allows the terrain to cast shadows onto itself and other objects.");
addProtectedField( "squareSize", TypeF32, Offset( mSquareSize, TerrainBlock ),
&TerrainBlock::_setSquareSize, &defaultProtectedGetFn,
"Indicates the spacing between points on the XY plane on the terrain." );
addProtectedField( "baseTexSize", TypeS32, Offset( mBaseTexSize, TerrainBlock ),
&TerrainBlock::_setBaseTexSize, &defaultProtectedGetFn,
"Size of base texture size per meter." );
addProtectedField( "lightMapSize", TypeS32, Offset( mLightMapSize, TerrainBlock ),
&TerrainBlock::_setLightMapSize, &defaultProtectedGetFn,
"Light map dimensions in pixels." );
addField( "screenError", TypeS32, Offset( mScreenError, TerrainBlock ), "Not yet implemented." );
endGroup( "Misc" );
Parent::initPersistFields();
removeField( "scale" );
Con::addVariable( "$TerrainBlock::debugRender", TypeBool, &smDebugRender, "Triggers debug rendering of terrain cells\n\n"
"@ingroup Terrain");
Con::addVariable( "$pref::Terrain::lodScale", TypeF32, &smLODScale, "A global LOD scale used to tweak the default terrain screen error value.\n\n"
"@ingroup Terrain");
Con::addVariable( "$pref::Terrain::detailScale", TypeF32, &smDetailScale, "A global detail scale used to tweak the material detail distances.\n\n"
"@ingroup Terrain");
}
void TerrainBlock::inspectPostApply()
{
Parent::inspectPostApply();
setMaskBits( MiscMask );
}
U32 TerrainBlock::packUpdate(NetConnection* con, U32 mask, BitStream *stream)
{
U32 retMask = Parent::packUpdate( con, mask, stream );
if ( stream->writeFlag( mask & TransformMask ) )
mathWrite( *stream, getTransform() );
if ( stream->writeFlag( mask & FileMask ) )
{
stream->write( mTerrFileName );
stream->write( mCRC );
}
if ( stream->writeFlag( mask & SizeMask ) )
stream->write( mSquareSize );
stream->writeFlag( mCastShadows );
if ( stream->writeFlag( mask & MaterialMask ) )
{
stream->write( mBaseTexSize );
stream->write( mLightMapSize );
}
stream->writeFlag( mask & HeightMapChangeMask );
if ( stream->writeFlag( mask & MiscMask ) )
stream->write( mScreenError );
return retMask;
}
void TerrainBlock::unpackUpdate(NetConnection* con, BitStream *stream)
{
Parent::unpackUpdate( con, stream );
if ( stream->readFlag() ) // TransformMask
{
MatrixF mat;
mathRead( *stream, &mat );
setTransform( mat );
}
if ( stream->readFlag() ) // FileMask
{
FileName terrFile;
stream->read( &terrFile );
stream->read( &mCRC );
if ( isProperlyAdded() )
setFile( terrFile );
else
mTerrFileName = terrFile;
}
if ( stream->readFlag() ) // SizeMask
stream->read( &mSquareSize );
mCastShadows = stream->readFlag();
if ( stream->readFlag() ) // MaterialMask
{
U32 baseTexSize;
stream->read( &baseTexSize );
if ( mBaseTexSize != baseTexSize )
{
mBaseTexSize = baseTexSize;
if ( isProperlyAdded() )
_updateBaseTexture( false );
}
U32 lightMapSize;
stream->read( &lightMapSize );
if ( mLightMapSize != lightMapSize )
{
mLightMapSize = lightMapSize;
if ( isProperlyAdded() )
{
SAFE_DELETE( mLightMap );
clearLightMap();
}
}
}
if ( stream->readFlag() && isProperlyAdded() ) // HeightMapChangeMask
{
_updateBounds();
_rebuildQuadtree();
_updatePhysics();
mDetailsDirty = true;
mLayerTexDirty = true;
}
if ( stream->readFlag() ) // MiscMask
stream->read( &mScreenError );
}
void TerrainBlock::getMinMaxHeight( F32 *minHeight, F32 *maxHeight ) const
{
// We can get the bound height from the last grid level.
const TerrainSquare *sq = mFile->findSquare( mFile->mGridLevels, 0, 0 );
*minHeight = fixedToFloat( sq->minHeight );
*maxHeight = fixedToFloat( sq->maxHeight );
}
//-----------------------------------------------------------------------------
// Console Methods
//-----------------------------------------------------------------------------
DefineEngineMethod( TerrainBlock, save, bool, ( const char* fileName),,
"@brief Saves the terrain block's terrain file to the specified file name.\n\n"
"@param fileName Name and path of file to save terrain data to.\n\n"
"@return True if file save was successful, false otherwise")
{
char filename[256];
dStrcpy(filename,fileName);
char *ext = dStrrchr(filename, '.');
if (!ext || dStricmp(ext, ".ter") != 0)
dStrcat(filename, ".ter");
return static_cast<TerrainBlock*>(object)->save(filename);
}
//ConsoleMethod(TerrainBlock, save, bool, 3, 3, "(string fileName) - saves the terrain block's terrain file to the specified file name.")
//{
// char filename[256];
// dStrcpy(filename,argv[2]);
// char *ext = dStrrchr(filename, '.');
// if (!ext || dStricmp(ext, ".ter") != 0)
// dStrcat(filename, ".ter");
// return static_cast<TerrainBlock*>(object)->save(filename);
//}
ConsoleDocFragment _getTerrainHeight1(
"@brief Gets the terrain height at the specified position\n\n"
"@param position The world space point, minus the z (height) value. Formatted as (\"x y\")\n\n"
"@return Returns the terrain height at the given point as an F32 value.\n\n"
"@ingroup Terrain",
NULL,
"bool getTerrainHeight( Point2I position );"
);
ConsoleDocFragment _getTerrainHeight2(
"@brief Gets the terrain height at the specified position\n\n"
"@param x The X coordinate in world space\n"
"@param y The Y coordinate in world space\n\n"
"@return Returns the terrain height at the given point as an F32 value.\n\n"
"@ingroup Terrain",
NULL,
"bool getTerrainHeight( F32 x, F32 y);"
);
ConsoleFunction(getTerrainHeight, F32, 2, 3, "(Point2 pos) - gets the terrain height at the specified position."
"@param pos The world space point, minus the z (height) value\n Can be formatted as either (\"x y\") or (x,y)\n"
"@return Returns the terrain height at the given point as an F32 value.\n"
"@hide")
{
Point2F pos;
F32 height = 0.0f;
if(argc == 2)
dSscanf(argv[1],"%f %f",&pos.x,&pos.y);
else if(argc == 3)
{
pos.x = dAtof(argv[1]);
pos.y = dAtof(argv[2]);
}
TerrainBlock * terrain = getTerrainUnderWorldPoint(Point3F(pos.x, pos.y, 5000.0f));
if(terrain)
if(terrain->isServerObject())
{
Point3F offset;
terrain->getTransform().getColumn(3, &offset);
pos -= Point2F(offset.x, offset.y);
terrain->getHeight(pos, &height);
}
return height;
}
ConsoleDocFragment _getTerrainHeightBelowPosition1(
"@brief Takes a world point and find the \"highest\" terrain underneath it\n\n"
"@param position The world space point, minus the z (height) value. Formatted as (\"x y\")\n\n"
"@return Returns the closest terrain height below the given point as an F32 value.\n\n"
"@ingroup Terrain",
NULL,
"bool getTerrainHeightBelowPosition( Point2I position );"
);
ConsoleDocFragment _getTerrainHeightBelowPosition2(
"@brief Takes a world point and find the \"highest\" terrain underneath it\n\n"
"@param x The X coordinate in world space\n"
"@param y The Y coordinate in world space\n\n"
"@return Returns the closest terrain height below the given point as an F32 value.\n\n"
"@ingroup Terrain",
NULL,
"bool getTerrainHeightBelowPosition( F32 x, F32 y);"
);
ConsoleFunction(getTerrainHeightBelowPosition, F32, 2, 4, "(Point3F pos) - gets the terrain height at the specified position."
"@param pos The world space point. Can be formatted as either (\"x y z\") or (x,y,z)\n"
"@note This function is useful if you simply want to grab the terrain height underneath an object.\n"
"@return Returns the terrain height at the given point as an F32 value.\n"
"@hide")
{
Point3F pos;
F32 height = 0.0f;
if(argc == 2)
dSscanf(argv[1], "%f %f %f", &pos.x, &pos.y, &pos.z);
else if(argc == 4)
{
pos.x = dAtof(argv[1]);
pos.y = dAtof(argv[2]);
pos.z = dAtof(argv[3]);
}
else
{
Con::errorf("getTerrainHeightBelowPosition(Point3F): Invalid argument count! Valid arguments are either \"x y z\" or x,y,z\n");
return 0;
}
TerrainBlock * terrain = getTerrainUnderWorldPoint(pos);
Point2F nohghtPos(pos.x, pos.y);
if(terrain)
{
if(terrain->isServerObject())
{
Point3F offset;
terrain->getTransform().getColumn(3, &offset);
nohghtPos -= Point2F(offset.x, offset.y);
terrain->getHeight(nohghtPos, &height);
}
}
return height;
}