Liquid wave depth - does this actually work? (Code spew)
by Kent Butler · in Torque Game Engine · 12/19/2007 (10:53 pm) · 16 replies
The code below implements a networked waterblock that allows the server to know how deep the water is WITH the wave depth - which also means clients have syncronized waves.
Working on a shapebase problem related to the mWaterCoverage calculation, I ended up going through a bit of the waterblock code. This made me realize that Torque currently can't tell how deep waves are on the water - which would be nice. A search of the various resources for a completed solution turned up negative. So I took a whack at it.
Waves are a math function over time. The fluid::Render() function sets a clocking variable telling the various functions (wave, flow, etc.) what point in time to use for the calculation. So all the server really needs in order to calculate wave depth is a F32 time counter that syncs with the client (aahh that's the rub!).
This solution isn't perfect, but it seems decent. It implements the "iTickable" interface using a single F32 sent every 100 ticks (chosen arbitrarily) to the client. The client still does it's own clocking - and just makes sure it's using the same basic point in time as the server for it's calculation. It seems to work perfectly (to the fractional millisecond) when playing on the server and pretty darn good on a LAN (>99% within .05 sec). The big question is if it will work well over a slower network connection.
So does anyone want to try it and see if I've made a crappy change or if it's actually useful? I would really love some feedback on the method. If it seems OK, I'll clean it up and put the source files up as a resource.
There are more descriptive comments in the code, and integration with shapebase is addressed after the fluid/waterblock changes are detailed. BTW: This is sort of advanced code and not newbie-friendly at the moment - if you get 1500 compiler errors, you're on your own, SO BACK STUFF UP!!!!!.
So here goes - hope I didn't miss anything.....
(find the surrounding code, and add/change the parts in bold unless directed otherwise):
....ooops .... exceeds 4096 characters - whatta pain, see the next few posts
Working on a shapebase problem related to the mWaterCoverage calculation, I ended up going through a bit of the waterblock code. This made me realize that Torque currently can't tell how deep waves are on the water - which would be nice. A search of the various resources for a completed solution turned up negative. So I took a whack at it.
Waves are a math function over time. The fluid::Render() function sets a clocking variable telling the various functions (wave, flow, etc.) what point in time to use for the calculation. So all the server really needs in order to calculate wave depth is a F32 time counter that syncs with the client (aahh that's the rub!).
This solution isn't perfect, but it seems decent. It implements the "iTickable" interface using a single F32 sent every 100 ticks (chosen arbitrarily) to the client. The client still does it's own clocking - and just makes sure it's using the same basic point in time as the server for it's calculation. It seems to work perfectly (to the fractional millisecond) when playing on the server and pretty darn good on a LAN (>99% within .05 sec). The big question is if it will work well over a slower network connection.
So does anyone want to try it and see if I've made a crappy change or if it's actually useful? I would really love some feedback on the method. If it seems OK, I'll clean it up and put the source files up as a resource.
There are more descriptive comments in the code, and integration with shapebase is addressed after the fluid/waterblock changes are detailed. BTW: This is sort of advanced code and not newbie-friendly at the moment - if you get 1500 compiler errors, you're on your own, SO BACK STUFF UP!!!!!.
So here goes - hope I didn't miss anything.....
(find the surrounding code, and add/change the parts in bold unless directed otherwise):
....ooops .... exceeds 4096 characters - whatta pain, see the next few posts
About the author
#2
Next, in waterblock.cc, WaterBlock::WaterBlock()
Next, in waterblock.cc, at the bottom of WaterBlock::initPersistFields()
Now in waterblock.cc, WaterBlock::packUpdate()
Obviously, next in waterblock.cc, WaterBlock::unpackUpdate()
Finally, at the bottom of waterblock.cc - insert the following:
12/19/2007 (10:54 pm)
Now, in waterBlock.h (near the top)// MM: Depth-map Resolution.
enum WaterAttributes
{
eDepthMapResolution = 512,
};
[B]
// ** KGB: Section added for water sync
enum States {
updateFluidMask = BIT(1),
updateTimeMask = BIT(2),
};
F32 getWaterSurface ( const Point3F &pos, bool worldSpace = true ) const;
protected:
// These three methods are the interface for ITickable
virtual void interpolateTick( F32 delta );
virtual void processTick();
virtual void advanceTime( F32 timeDelta );
private:
F32 mStartSeconds; // Time the object was created (server or Ghost)
S32 mClientSyncTicks; // How often to sync client time (in ticks)
F32 mAllowedClientVariance; // Amount client needs to out of sync to resync
F32 sElapsedSeconds; // Elapsed time on the server
F32 cBaseSeconds; // Amount to adjust local time count
F32 cElapsedSeconds; // Elapsed time on the client (unadjusted)
// ** KGB end **
[/B]
fluid mFluid;
bool mTile;
TextureHandle mSurfaceTexture;Next, in waterblock.cc, WaterBlock::WaterBlock()
WaterBlock::WaterBlock()
{
mNetFlags.set(Ghostable | ScopeAlways);
mTypeMask = WaterObjectType;
[B]
// **KGB: Waterblock sync vars
mStartSeconds = SECONDS; // Start time of actual object (server or ghost)
mClientSyncTicks = 100; // How often to sync client time (in ticks)
mAllowedClientVariance = .05; // how far out of syc the client needs to be to mess with the
// base rate (resync causes a jitter)
sElapsedSeconds = 0.0f; // Time elapsed on the server
cBaseSeconds = 0.0f; // Difference between client start and server start
cElapsedSeconds = 0.0f; // Client-side elapsed time counter (mostly redundant with fluid)
// **KGB end
[/B]
mObjBox.min.set( 0, 0, 0 );
mObjBox.max.set( 1, 1, 1 );Next, in waterblock.cc, at the bottom of WaterBlock::initPersistFields()
addField( "specularColor", TypeColorF, Offset( mSpecColor, WaterBlock ) );
addField( "specularPower", TypeF32, Offset( mSpecPower, WaterBlock ) );
endGroup("Misc");
[B]
// **KGB: Water sync network control params
addGroup("Sync");
addField( "syncTicks", TypeS32, Offset( mClientSyncTicks, WaterBlock));
addField( "allowedVariance", TypeF32, Offset(mAllowedClientVariance,WaterBlock));
endGroup("Sync");
[/B]Now in waterblock.cc, WaterBlock::packUpdate()
U32 WaterBlock::packUpdate( NetConnection* c, U32 mask, BitStream* stream )
{
U32 retMask = Parent::packUpdate( c, mask, stream );
[B]
stream->write(sElapsedSeconds); // KGB - always send this
if(stream->writeFlag(mask & updateFluidMask))
{
[/B]
stream->writeFlag(mTile);
stream->writeAffineTransform( mObjToWorld );
.
.
.
[B]
} // <---- We are adding an if block ... make sure to close it at the end of the statements
[/B]
} <--- And the one to close the function!Obviously, next in waterblock.cc, WaterBlock::unpackUpdate()
void WaterBlock::unpackUpdate( NetConnection* c, BitStream* stream )
{
Parent::unpackUpdate( c, stream );
U32 LiquidType;
[B]
// ***KGB: Water Sync Code
stream->read(&sElapsedSeconds); // These are the seconds that the server thinks has elapsed
F32 ckB = mFabs(mFluid.m_ServerSeconds - sElapsedSeconds); // The difference between client and server
// Only set the base seconds once unless we are out of sync. Setting it hard
// like this can cause a bit of a jump - it could be made better with interpolation.
//
if ( cBaseSeconds == 0 || ckB > mAllowedClientVariance ){
cBaseSeconds = sElapsedSeconds - (SECONDS - mStartSeconds);;
}
// Check if the rest of the waterBlock needs updating
if(stream->readFlag())
{
[/B]
mTile = stream->readFlag();
stream->readAffineTransform( &mObjToWorld );
.
.
.
.
resetWorldBox();
[B]
} // <---- We are adding an if block ... make sure to close it at the end of the statements
[/B]
} <--- And the one to close the function!Finally, at the bottom of waterblock.cc - insert the following:
// *****************************************************************************
// KGB - These functions implement the iTickable interface to allow the server to
// sync the wave functions with clients. A time counter on the server is used to
// calculate values in the fluid. The server sends the client this elapsed time.
// The client compares this to it's own local waterblock counter to calculate the
// difference between the two. Since the server always starts before the client,
// adding this differental to the local counter results in a rather close sync
// between server and client.
//
// The server continues to send out a sync occasionally. The client will only
// recalculate the differental if it falls outside the defined threshold. If it
// is too far out of sync the client will adjust the differental (which can cause
// a rendering jump) to keep the rendering and physics in sync. Some minor
// interpolation when adjusting differental would be the final piece to this.
//
// NOTE: This is currently used to extract surface Z (wave depth) but all
// time-based effects applied to the waterblock should be accessable. Generally
// they seem to be computed in the runQuadTree function and the formulas can be
// used to compute a single point value (for current time) if taken out of that loop.
// -----------------------------------------------------------------------------
void WaterBlock::interpolateTick( F32 delta ){
// This page intentionally left blank.
}
// processTick is used to make sure the clients are on the same page
// with the server every so often - but not every tick!
// ------------------------------------------------------------------------------
static S32 ctr=0;
void WaterBlock::processTick(){
if( isServerObject() ) {
if(ctr < mClientSyncTicks)
ctr++;
else {
setMaskBits(updateTimeMask);
ctr=0;
}
}
}
// Advance time takes over from the fluid's render function in keeping track of time.
// The server keeps local time rolled into the local fluid object for physics and clocking.
// The client keeps track of the ghosted water block's timing syncronized with the server
// and uses this for rendering.
// ------------------------------------------------------------------------------
void WaterBlock::advanceTime( F32 timeDelta ){
if (isServerObject()){
sElapsedSeconds = SECONDS - mStartSeconds; // This is the # of elapsed seconds since the waterblock started
mFluid.m_ServerSeconds = sElapsedSeconds; // This is the reference fluid block
} else {
cElapsedSeconds = SECONDS - mStartSeconds; // Actual time elapsed on client
mFluid.m_ServerSeconds = cElapsedSeconds + cBaseSeconds; // Time adjusted for server offset
}
}
// Find an actual surface level of the water with the waves included.
// Not much to explain here - the work goes on in fluidSupport.cc.
// ------------------------------------------------------------------------------
F32 WaterBlock::getWaterSurface(const Point3F &pos, bool worldSpace) const
{
Point3F Pos = pos;
if( worldSpace )
{
Pos.x += F32(mTerrainHalfSize);
Pos.y += F32(mTerrainHalfSize);
}
F32 surface = mFluid.surfaceAtXY(Pos.x, Pos.y);
return( surface );
}
// *****************************************************************************
#3
From a practical standpoint, these changes add one useful function: WaterBlock::getWaterSurface() that takes a point3F and tells you the Z of the water surface at that point. Objects still need to be changed to take advantage of this.
Implementing this with shapebase adds functionality to many game objects (vehicle, player, item, staticShape, etc.) making the mWatercoverage variable represent the coverage including wave height. The shapebase updates water-related variables in the ShapeBase::updateContainer() and ShapeBase::WaterFind() functions, so this is the place to implement. .... however ....
.... ShapeBase::UpdateContainer fails when the object is far-enough beneath the surface (it's failure started this adventure) - so I recently rewrote it and replaced the waterFind() function. I've included the "checkInLiquid" function from that patch - modified to get wave depth. If you prefer to use the original Torque code - it should be pretty easy to integrate into ShapeBase::waterFind by updating the "isPointSubmergedSimple()" check with "getWaterSurface()" and the logic below.
The patch is here: www.garagegames.com/mg/forums/result.thread.php?qt=21868
To use the following code, you must first do the patch linked to above!
And that's pretty much it.
12/19/2007 (10:55 pm)
USING THE CHANGES:From a practical standpoint, these changes add one useful function: WaterBlock::getWaterSurface() that takes a point3F and tells you the Z of the water surface at that point. Objects still need to be changed to take advantage of this.
Implementing this with shapebase adds functionality to many game objects (vehicle, player, item, staticShape, etc.) making the mWatercoverage variable represent the coverage including wave height. The shapebase updates water-related variables in the ShapeBase::updateContainer() and ShapeBase::WaterFind() functions, so this is the place to implement. .... however ....
.... ShapeBase::UpdateContainer fails when the object is far-enough beneath the surface (it's failure started this adventure) - so I recently rewrote it and replaced the waterFind() function. I've included the "checkInLiquid" function from that patch - modified to get wave depth. If you prefer to use the original Torque code - it should be pretty easy to integrate into ShapeBase::waterFind by updating the "isPointSubmergedSimple()" check with "getWaterSurface()" and the logic below.
The patch is here: www.garagegames.com/mg/forums/result.thread.php?qt=21868
To use the following code, you must first do the patch linked to above!
// KGB:
// This is based on the player::pointInWater function. It gets a list of all water blocks,
// and checks each to see if the object is in the water. If so, it sets up the various
// water-related variables like the old waterFind()/updateContainer() functions.
//
bool ShapeBase::checkInLiquid()
{
SimpleQueryList sql;
if (isServerObject())
gServerSceneGraph->getWaterObjectList(sql);
else
gClientSceneGraph->getWaterObjectList(sql);
for (U32 i = 0; i < sql.mList.size(); i++)
{
WaterBlock* pBlock = dynamic_cast<WaterBlock*>(sql.mList[i]);
if (pBlock)
{
Point3F pos = getPosition();
F32 wS = pBlock->getWaterSurface(pos);
if (wS > 0.0f) {
// There is some water here - let's see if we are in it
const Box3F& sbox = getWorldBox(); // Object box
if (wS < sbox.max.z) // If surface is lower than the object top
mWaterCoverage = mClampF((wS - sbox.min.z) / (sbox.max.z - sbox.min.z),0.0f,1.0f);
else
// Otherwise, we're over our head
mWaterCoverage = 1.0f;
if (mWaterCoverage >= 0.1f) {
// If we are deep enough - calculate the density and viscocity
F32 viscosity = pBlock->getViscosity();
F32 density = pBlock->getDensity();
mDrag = mDataBlock->drag * viscosity * mWaterCoverage;
mBuoyancy = (density / mDataBlock->density) * mWaterCoverage;
}
// Look up the liquid's particulars for the object's pleasure.
mLiquidType = pBlock->getLiquidType();
mLiquidHeight = pBlock->getSurfaceHeight();
return true;
}
}
}
return false;
}And that's pretty much it.
#4
12/19/2007 (11:02 pm)
Holy crapola - the friggin computer's right - that WAS big.
#5
12/19/2007 (11:18 pm)
Interesting idea and implementation. I like the idea of synced water waves. :)
#6
01/07/2008 (7:50 pm)
Well since there are no fluid files, I guess this is not a working system for TGEA. Unless someone has already done it perhaps?
#7
01/08/2008 (12:01 am)
No, water in TGEA is a plane only.
#9
Edit->
Edit2-> Tried implementing this, but now I can't get any waves at all. I'm thinking that either I deleted the calculation code (doubt it), or I'm not packing/unpacking the updates for some reason (more likely, because I can't change the water block in the World Editor)
Edit3-> The packing/unpacking was the reason that I couldn't edit it. Two more problems: 1. When partially in water, the collision box is messed up. It looks like the engine is trying to keep the collision box all the way under, or completely out. 2. There is no wave calculation after the initial one, so the waves are static, and not animated. It seems like the time used to wave calculation is never increased.
Edit4-> If I increase m_ServerSeconds right before I set m_Seconds equal to it, than I get the waves, but the water hight isn't calculated on the server still. So floating things wont follow the waves.
06/14/2008 (4:04 pm)
Will this just sync the clients' waves, or will it allow the server to sea waves? (Get it, sea waves =P)Edit->
Quote:Implementing this with shapebase adds functionality to many game objects (vehicle, player, item, staticShape, etc.) making the mWatercoverage variable represent the coverage including wave height.Never mind.
Edit2-> Tried implementing this, but now I can't get any waves at all. I'm thinking that either I deleted the calculation code (doubt it), or I'm not packing/unpacking the updates for some reason (more likely, because I can't change the water block in the World Editor)
Edit3-> The packing/unpacking was the reason that I couldn't edit it. Two more problems: 1. When partially in water, the collision box is messed up. It looks like the engine is trying to keep the collision box all the way under, or completely out. 2. There is no wave calculation after the initial one, so the waves are static, and not animated. It seems like the time used to wave calculation is never increased.
Edit4-> If I increase m_ServerSeconds right before I set m_Seconds equal to it, than I get the waves, but the water hight isn't calculated on the server still. So floating things wont follow the waves.
#10
#include "core/iTickable.h"
and changing
class WaterBlock : public SceneObject
to
class WaterBlock : public SceneObject, public virtual ITickable
in waterBlock.h otherwise processTick and advanceTime won't be called and the water won't move as reported by Nathan Kent.
07/07/2008 (8:43 am)
One thing is missing from this code, you have to make the WaterBlock tickable by adding#include "core/iTickable.h"
and changing
class WaterBlock : public SceneObject
to
class WaterBlock : public SceneObject, public virtual ITickable
in waterBlock.h otherwise processTick and advanceTime won't be called and the water won't move as reported by Nathan Kent.
#11
Edit -> The water is ticking, but the server still isn't seeing the waves.
07/07/2008 (8:45 am)
Thanks! Looking through it, I'd found that it doesn't tick, but I couldn't find out how to make it. Let's see if this works...Edit -> The water is ticking, but the server still isn't seeing the waves.
#12
Make sure any calculations use mFluid.m_ServerSeconds instead of SECONDS. My code has diverged from the latest so I can't tell you exactly what code to change.
07/07/2008 (10:00 am)
Nathan,Make sure any calculations use mFluid.m_ServerSeconds instead of SECONDS. My code has diverged from the latest so I can't tell you exactly what code to change.
#13
07/07/2008 (2:48 pm)
Ok, the server can see the waves now, but they're not on the same scale as the clients. The waves I see go up much higher, and down much lower than the player is moving.
#14
In fuildSupport.cc m_WaveFactor get set to m_WaveAmplitude * 0.25f;
I'm not sure if this has anything to do with it but I didn't use Kent's masks. In waterBlock.h I have:
enum
{
updateTimeMask = Parent::NextFreeMask << 0,
updateFluidMask = ~updateTimeMask, // Update all the parameters if not just a timer update
NextFreeMask = Parent::NextFreeMask << 1
};
This more closely follows Torque's convention of mask allocation.
Also, if you change the wave amplitude in the editor it is only changed on the server and does not correctly change the value on the client. You have to save the mission, exit, and reload the mission again the value will be correct on both the server and the client. I've fixed this on my version and will post this when I get a chance.
07/08/2008 (11:14 am)
Are you using m_WaveAmplitude to calculate the wave height? You need to use m_WaveFactor.In fuildSupport.cc m_WaveFactor get set to m_WaveAmplitude * 0.25f;
I'm not sure if this has anything to do with it but I didn't use Kent's masks. In waterBlock.h I have:
enum
{
updateTimeMask = Parent::NextFreeMask << 0,
updateFluidMask = ~updateTimeMask, // Update all the parameters if not just a timer update
NextFreeMask = Parent::NextFreeMask << 1
};
This more closely follows Torque's convention of mask allocation.
Also, if you change the wave amplitude in the editor it is only changed on the server and does not correctly change the value on the client. You have to save the mission, exit, and reload the mission again the value will be correct on both the server and the client. I've fixed this on my version and will post this when I get a chance.
#15
Few things:
1. The splash particles don't see the waves (you'll start splashing at the same hight everywhere)
2. Objects following the waves don't look all that realistic.
3. Wierd modification of collision boxes
I'm going to work on all of those, and see if my thoughts on how to fix the "realisticness" are right (using an upwards/downwards velocity to make Players follow the waves).
07/08/2008 (1:04 pm)
Yeah, problem solved, and all I had to do was save and reload =PFew things:
1. The splash particles don't see the waves (you'll start splashing at the same hight everywhere)
2. Objects following the waves don't look all that realistic.
3. Wierd modification of collision boxes
I'm going to work on all of those, and see if my thoughts on how to fix the "realisticness" are right (using an upwards/downwards velocity to make Players follow the waves).
#16
This will cause fluid::IsFuildAtXY to give incorrect results if you go outside the range 0 to 2047 in the x or y direction.
Just add the line:
mTile = tiling;
as the first line of the function fluid::SetInfo and this will fix the problem.
07/14/2008 (1:42 pm)
I've just discovered a bug in fluid::SetInfo. The variable tiling is passed but mTile is never set equal to tiling.This will cause fluid::IsFuildAtXY to give incorrect results if you go outside the range 0 to 2047 in the x or y direction.
Just add the line:
mTile = tiling;
as the first line of the function fluid::SetInfo and this will fix the problem.
Torque Owner Kent Butler
//------------------------------------------------------------------------------ // Public Functions // public: fluid ( void ); ~fluid (); [B] // // KGB: Check the surface Z at point XY. // F32 surfaceAtXY ( f32 X, f32 Y ) const; F32 m_ServerSeconds; // KGB - Server time sync. [/B] // // Render (in FluidRender.cc): // void Render ( bool& EyeSubmerged );In fluidRender.cc, fluid::Render()
f32 TLMap[] = { 0.0f, Q2, 0.0f, 0.0f }; // Several attributes in the fluid vary over time. Get a definitive time // reading now to be used throughout this render pass. [B]m_Seconds = m_ServerSeconds; // KGB: Use server time instead of computing it[/B] // Based on the view frustrum, accumulate the list of triangles that // comprise the fluid surface for this render pass. RunQuadTree( EyeSubmerged );In fluidSupport.cc
fluid::fluid( void ) { m_Instances += 1; [B]m_ServerSeconds = 0.0f; // KGB: Networking sync[/B] // Fill out fields with a stable, if useless, state. m_SquareX0 = 0; m_SquareY0 = 0;Then at the bottom of fluidSupport.cc add:
//============================================================================== // KGB: This returns the wave-adjusted surface Z position for a given XY // returns 0 if there is no water (so if the water Z is actually 0.0, it gets a minor // goose to make sure the routine doesn't completely fail). F32 fluid::surfaceAtXY( f32 X, f32 Y ) const { F32 surfaceZ = 0.0f; if (IsFluidAtXY(X, Y)){ // this uses the formula from the runQuadTree() routine that calculates a // point matrix for rendering - here we only need to calculate one point. // surfaceZ = m_SurfaceZ + (SINE( (X * 0.05f) + m_ServerSeconds ) + SINE( (Y * 0.05f) + m_ServerSeconds)) * m_WaveFactor; // Failure trap if (surfaceZ == 0.0f) surfaceZ = 0.001f; // what are the odds? } return(surfaceZ); } //==============================================================================