/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 Electronic Arts Inc.
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see .
*/
////////////////////////////////////////////////////////////////////////////////
// //
// (c) 2001-2003 Electronic Arts Inc. //
// //
////////////////////////////////////////////////////////////////////////////////
// FILE: W3DTankDraw.cpp //////////////////////////////////////////////////////////////////////////
// Draw turreted tanks
// Michael S. Booth, October 2001
///////////////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES ///////////////////////////////////////////////////////////////////////////////////////
#include
#include
#include "Common/Thing.h"
#include "Common/ThingFactory.h"
#include "Common/GameAudio.h"
#include "Common/ThingTemplate.h"
#include "Common/Xfer.h"
#include "GameLogic/Weapon.h"
#include "GameLogic/GameLogic.h"
#include "GameLogic/Module/PhysicsUpdate.h"
#include "GameLogic/Module/BodyModule.h"
#include "GameLogic/ScriptEngine.h"
#include "GameLogic/Module/AIUpdate.h"
#include "GameClient/Drawable.h"
#include "GameClient/ParticleSys.h"
#include "W3DDevice/GameClient/W3DGameClient.h"
#include "W3DDevice/GameClient/Module/W3DTankDraw.h"
#include "WW3D2/matinfo.h"
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes")
#endif
class Matrix3D;
//-------------------------------------------------------------------------------------------------
W3DTankDrawModuleData::W3DTankDrawModuleData() :
m_treadDebrisNameLeft("TrackDebrisDirtLeft"),
m_treadDebrisNameRight("TrackDebrisDirtRight"),
m_treadAnimationRate(0.0f),
m_treadPivotSpeedFraction(0.6f),
m_treadDriveSpeedFraction(0.3f)
{
}
//-------------------------------------------------------------------------------------------------
W3DTankDrawModuleData::~W3DTankDrawModuleData()
{
}
//-------------------------------------------------------------------------------------------------
void W3DTankDrawModuleData::buildFieldParse(MultiIniFieldParse& p)
{
W3DModelDrawModuleData::buildFieldParse(p);
static const FieldParse dataFieldParse[] =
{
{ "TreadDebrisLeft", INI::parseAsciiString, NULL, offsetof(W3DTankDrawModuleData, m_treadDebrisNameLeft) },
{ "TreadDebrisRight", INI::parseAsciiString, NULL, offsetof(W3DTankDrawModuleData, m_treadDebrisNameRight) },
{ "TreadAnimationRate", INI::parseVelocityReal, NULL, offsetof(W3DTankDrawModuleData, m_treadAnimationRate) },
{ "TreadPivotSpeedFraction", INI::parseReal, NULL, offsetof(W3DTankDrawModuleData, m_treadPivotSpeedFraction) },
{ "TreadDriveSpeedFraction", INI::parseReal, NULL, offsetof(W3DTankDrawModuleData, m_treadDriveSpeedFraction) },
{ 0, 0, 0, 0 }
};
p.add(dataFieldParse);
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
W3DTankDraw::W3DTankDraw( Thing *thing, const ModuleData* moduleData )
: W3DModelDraw( thing, moduleData ),m_prevRenderObj(NULL), m_treadDebrisLeft(NULL), m_treadDebrisRight(NULL)
{
m_treadDebrisLeft = NULL;
m_treadDebrisRight = NULL;
for (Int i=0; iattachToObject(NULL);
m_treadDebrisLeft->destroy();
m_treadDebrisLeft = NULL;
}
if (m_treadDebrisRight)
{
m_treadDebrisRight->attachToObject(NULL);
m_treadDebrisRight->destroy();
m_treadDebrisRight = NULL;
}
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void W3DTankDraw::createEmitters( void )
{
if (!m_treadDebrisLeft)
{
const ParticleSystemTemplate *sysTemplate;
sysTemplate = TheParticleSystemManager->findTemplate(getW3DTankDrawModuleData()->m_treadDebrisNameLeft);
if (sysTemplate)
{
m_treadDebrisLeft = TheParticleSystemManager->createParticleSystem( sysTemplate );
m_treadDebrisLeft->attachToDrawable(getDrawable());
// important: mark it as do-not-save, since we'll just re-create it when we reload.
m_treadDebrisLeft->setSaveable(FALSE);
// they come into being stopped.
m_treadDebrisLeft->stop();
}
}
if (!m_treadDebrisRight)
{
const ParticleSystemTemplate *sysTemplate;
sysTemplate = TheParticleSystemManager->findTemplate(getW3DTankDrawModuleData()->m_treadDebrisNameRight);
if (sysTemplate)
{
m_treadDebrisRight = TheParticleSystemManager->createParticleSystem( sysTemplate );
m_treadDebrisRight->attachToDrawable(getDrawable());
// important: mark it as do-not-save, since we'll just re-create it when we reload.
m_treadDebrisRight->setSaveable(FALSE);
// they come into being stopped.
m_treadDebrisRight->stop();
}
}
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
W3DTankDraw::~W3DTankDraw()
{
for (Int i=0; iisDrawableEffectivelyHidden())
return;
if (m_treadDebrisLeft)
m_treadDebrisLeft->start();
if (m_treadDebrisRight)
m_treadDebrisRight->start();
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
/**
* Stop creating debris from the tank treads
*/
void W3DTankDraw::stopMoveDebris( void )
{
if (m_treadDebrisLeft)
m_treadDebrisLeft->stop();
if (m_treadDebrisRight)
m_treadDebrisRight->stop();
}
//-------------------------------------------------------------------------------------------------
void W3DTankDraw::setHidden(Bool h)
{
W3DModelDraw::setHidden(h);
if (h)
{
stopMoveDebris();
}
}
//-------------------------------------------------------------------------------------------------
void W3DTankDraw::setFullyObscuredByShroud(Bool fullyObscured)
{
if (fullyObscured != getFullyObscuredByShroud())
{
if (fullyObscured)
stopMoveDebris();
}
W3DModelDraw::setFullyObscuredByShroud(fullyObscured);
}
/**Update uv coordinates on each tread object to simulate movement*/
void W3DTankDraw::updateTreadPositions(Real uvDelta)
{
Real offset_u;
TreadObjectInfo *pTread=m_treads;
for (Int i=0; im_type == TREAD_LEFT) //this tread needs to scroll forwards
offset_u = pTread->m_materialSettings.customUVOffset.X + uvDelta;
else
if (pTread->m_type == TREAD_RIGHT) //this tread needs to scroll backwards
offset_u = pTread->m_materialSettings.customUVOffset.X - uvDelta;
// ensure coordinates of offset are in [0, 1] range:
offset_u = offset_u - WWMath::Floor(offset_u);
pTread->m_materialSettings.customUVOffset.Set(offset_u,0);
pTread++;
}
}
/**Grab pointers to the sub-meshes for each tread*/
void W3DTankDraw::updateTreadObjects(void)
{
RenderObjClass *robj=getRenderObject();
//clear all previous tread pointers
for (Int i=0; im_treadAnimationRate && robj)
{
for (Int i=0; i < robj->Get_Num_Sub_Objects() && m_treadCount < MAX_TREADS_PER_TANK; i++)
{
RenderObjClass *subObj=robj->Get_Sub_Object(i);
const char *meshName;
//Check if subobject name starts with "TREADS".
if (subObj && subObj->Class_ID() == RenderObjClass::CLASSID_MESH && subObj->Get_Name()
&& ( (meshName=strchr(subObj->Get_Name(),'.') ) != 0 && *(meshName++))
&&_strnicmp(meshName,"TREADS", 6) == 0)
{ //check if sub-object has the correct material to do texture scrolling.
MaterialInfoClass *mat=subObj->Get_Material_Info();
if (mat)
{ for (Int j=0; jVertex_Material_Count(); j++)
{
VertexMaterialClass *vmaterial=mat->Peek_Vertex_Material(j);
LinearOffsetTextureMapperClass *mapper=(LinearOffsetTextureMapperClass *)vmaterial->Peek_Mapper();
if (mapper && mapper->Mapper_ID() == TextureMapperClass::MAPPER_ID_LINEAR_OFFSET)
{ mapper->Set_UV_Offset_Delta(Vector2(0,0)); //disable automatic scrolling
subObj->Add_Ref(); //increase reference since we're storing the pointer
m_treads[m_treadCount].m_robj=subObj;
m_treads[m_treadCount].m_type = TREAD_MIDDLE; //default type
subObj->Set_User_Data(&m_treads[m_treadCount].m_materialSettings); //tell W3D about custom material settings
m_treads[m_treadCount].m_materialSettings.customUVOffset=Vector2(0,0);
switch (meshName[6]) //check next character after 'TREADS'
{
case 'L':
case 'l': m_treads[m_treadCount].m_type = TREAD_LEFT;
break;
case 'R':
case 'r': m_treads[m_treadCount].m_type = TREAD_RIGHT;
break;
}
m_treadCount++;
}
}
REF_PTR_RELEASE(mat);
}
}
REF_PTR_RELEASE(subObj);
}
}
m_prevRenderObj = robj;
}
//-------------------------------------------------------------------------------------------------
void W3DTankDraw::onRenderObjRecreated(void)
{
updateTreadObjects();
}
//-------------------------------------------------------------------------------------------------
/** Map behavior states into W3D animations. */
//-------------------------------------------------------------------------------------------------
void W3DTankDraw::doDrawModule(const Matrix3D* transformMtx)
{
const Real DEBRIS_THRESHOLD = 0.00001f;
Bool frozen = TheTacticalView->isTimeFrozen() && !TheTacticalView->isCameraMovementFinished();
frozen = frozen || TheScriptEngine->isTimeFrozenDebug() || TheScriptEngine->isTimeFrozenScript();
if (frozen)
return;
if (getRenderObject()==NULL) return;
if (getRenderObject() != m_prevRenderObj) {
updateTreadObjects();
}
// get object from logic
Object *obj = getDrawable()->getObject();
if (obj == NULL)
return;
// get object physics state
PhysicsBehavior *physics = obj->getPhysics();
if (physics == NULL)
return;
const Coord3D *vel = physics->getVelocity();
// if tank is moving, kick up dust and debris
Real velMag = vel->x*vel->x + vel->y*vel->y; // only care about moving on the ground
if (velMag > DEBRIS_THRESHOLD && !getDrawable()->isDrawableEffectivelyHidden() && !getFullyObscuredByShroud())
startMoveDebris();
else
stopMoveDebris();
// kick debris higher the faster we move
Coord3D velMult;
velMag = (Real)sqrt( velMag );
velMult.x = 0.5f * velMag + 0.1f;
if (velMult.x > 1.0f)
velMult.x = 1.0f;
velMult.y = velMult.x;
velMult.z = velMag + 0.1f;
if (velMult.z > 1.0f)
velMult.z = 1.0f;
m_treadDebrisLeft->setVelocityMultiplier( &velMult );
m_treadDebrisRight->setVelocityMultiplier( &velMult );
m_treadDebrisLeft->setBurstCountMultiplier( velMult.z );
m_treadDebrisRight->setBurstCountMultiplier( velMult.z );
//Update movement of treads
if (m_treadCount)
{
PhysicsTurningType turn=physics->getTurning();
Real offset_u;
Real treadScrollSpeed=getW3DTankDrawModuleData()->m_treadAnimationRate;
TreadObjectInfo *pTread=m_treads;
Real maxSpeed=obj->getAIUpdateInterface()->getCurLocomotorSpeed();
//For optimization sake, we only do complex tread scrolling when tank
//is mostly stationary and turning
if (turn != TURN_NONE && physics->getVelocityMagnitude()/maxSpeed < getW3DTankDrawModuleData()->m_treadPivotSpeedFraction)
{
//Check if we have turned enough since last draw to require animation
Coord3D dir;
obj->getUnitDirectionVector2D(dir);
Real angleToGoal = dir.x * m_lastDirection.x + dir.y * m_lastDirection.y;
if (fabs(1.0f-angleToGoal) > 0.00001f) //check if difference in angle cosines is greater than some cutoff.
{
if (turn == TURN_NEGATIVE) //turning right
updateTreadPositions(-treadScrollSpeed);
else //turning left
updateTreadPositions(treadScrollSpeed);
}
m_lastDirection=dir; //update for next frame
}
else
if (physics->isMotive() && physics->getVelocityMagnitude()/maxSpeed >= getW3DTankDrawModuleData()->m_treadDriveSpeedFraction)
{ //do simple scrolling based only on speed when tank is moving straight at high speed.
//we stop scrolling when tank slows down to reduce the appearance of sliding
//tread scrolling speed was not directly tied into tank velocity because it looked odd
//under certain situations when tank moved sideways.
for (Int i=0; im_materialSettings.customUVOffset.X - treadScrollSpeed;
// ensure coordinates of offset are in [0, 1] range:
offset_u = offset_u - WWMath::Floor(offset_u);
pTread->m_materialSettings.customUVOffset.Set(offset_u,0);
pTread++;
}
}
}
W3DModelDraw::doDrawModule(transformMtx);
}
// ------------------------------------------------------------------------------------------------
/** CRC */
// ------------------------------------------------------------------------------------------------
void W3DTankDraw::crc( Xfer *xfer )
{
// extend base class
W3DModelDraw::crc( xfer );
} // end crc
// ------------------------------------------------------------------------------------------------
/** Xfer method
* Version Info:
* 1: Initial version */
// ------------------------------------------------------------------------------------------------
void W3DTankDraw::xfer( Xfer *xfer )
{
// version
XferVersion currentVersion = 1;
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );
// extend base class
W3DModelDraw::xfer( xfer );
// John A and Mark W say there is no data to save here
} // end xfer
// ------------------------------------------------------------------------------------------------
/** Load post process */
// ------------------------------------------------------------------------------------------------
void W3DTankDraw::loadPostProcess( void )
{
// extend base class
W3DModelDraw::loadPostProcess();
// toss any existing ones and re-create 'em (since this module expects 'em to always be around)
tossEmitters();
createEmitters();
} // end loadPostProcess