Models on VGUI Panels
January 2024
You can help by adding links to this article from other relevant articles.
January 2024
This article shows how to render a player model to a custom VGUI image panel, the player model can be shown with a weapon and any animation. This can be seen in the Counter-Strike: Source character selection menu, thanks to Matt Boone for the information about this. This code is from the Advanced SDK, but modifications to work with HL2DM should be minimal.
It involves editing sdk_clientmode.cpp, teammenu.cpp, and teammenu.h (or as appropriate for your mod). The basic premise is to generate a global list of a custom image panel, checking if any of the panels are visible and then drawing the model they reference.
Possible expansions to this feature are to allow specification of the weapon model and animations required in the .res file entry.
This article provides full code listings and an example .res entry, to make use of this feature you will need to have some form of VGUI panel, such as a team or equipment menu.
Contents
We'll define the custom image panel and the global list in our team menu header and cpp file - there's nothing too complicated here so we'll whip through it quickly!
The header file entry is easy to get your head around, we declare a new class that inherits from vgui::ImagePanel, we also extern a CUtlVector for a global list of our custom image panels (used in clientmode)
class CClassImagePanel : public vgui::ImagePanel
{
public:
typedef vgui::ImagePanel BaseClass;
CClassImagePanel( vgui::Panel* pParent, const char* pName );
virtual ~CClassImagePanel();
virtual void ApplySettings( KeyValues* inResourceData );
virtual void Paint();
public:
char m_ModelName[128];
};
extern CUtlVector<CClassImagePanel*> g_ClassImagePanels;
Here's the core of our new custom image panel, the two most important functions are: ApplySettings where we find the name of the model that should be drawn with this panel and CreateControlByName which is required by any VGUI panel that has one of these image panels attached to it. We also define the global list of image panels, as described earlier.
CUtlVector<CClassImagePanel*> g_ClassImagePanels;
CClassImagePanel::CClassImagePanel( vgui::Panel* pParent, const char* pName )
: vgui::ImagePanel( pParent, pName )
{
g_ClassImagePanels.AddToTail( this );
m_ModelName[0] = 0;
}
CClassImagePanel::~CClassImagePanel()
{
g_ClassImagePanels.FindAndRemove( this );
}
void CClassImagePanel::ApplySettings( KeyValues* inResourceData )
{
const char* pName = inResourceData->GetString( 3DModel );
if ( pName )
V_strncpy( m_ModelName, pName, sizeof( m_ModelName ) );
BaseClass::ApplySettings( inResourceData );
}
void CClassImagePanel::Paint()
{
BaseClass::Paint();
}
Panel* TeamMenu::CreateControlByName(const char* controlName)
{
if ( V_stricmp( controlName, ClassImagePanel ) == 0 )
return new CClassImagePanel( NULL, controlName );
return BaseClass::CreateControlByName( controlName );
}
sdk_clientmode.cpp
This code is set out in the order of declaration in the CPP file - alternatively, you could declare prototypes of the utility functions and the update function then define them at the bottom of the file.
Declare handles to a baseanimatingoverlay for our player model and a baseanimating for our weapon model.
CHandle<C_BaseAnimatingOverlay> g_ClassImagePlayer; // player
CHandle<C_BaseAnimating> g_ClassImageWeapon; // weapon
Helper Functions
Utility function to let us know if a panel on our global list is visible or not - this dictates whether or not we should continue and draw the model.
// Utility to determine if the vgui panel is visible
bool WillPanelBeVisible( vgui::VPANEL hPanel )
{
while ( hPanel )
{
if ( !vgui::ipanel()->IsVisible( hPanel ) )
return false;
hPanel = vgui::ipanel()->GetParent( hPanel );
}
return true;
}
Utility function to check if we should recreate the model data
// Called to see if we should be creating or recreating the model instances
bool ShouldRecreateClassImageEntity( C_BaseAnimating* pEnt, const char* pNewModelName )
{
if ( !pNewModelName || !pNewModelName[0] )
return false;
if ( !pEnt )
return true;
const model_t* pModel = pEnt->GetModel();
if ( !pModel )
return true;
const char* pName = modelinfo->GetModelName( pModel );
if ( !pName )
return true;
// reload only if names are different
return( V_stricmp( pName, pNewModelName ) != 0 );
}
UpdateClassImageEntity
This is our largest function, it sets the animation to play for the upper and lower sections of our player model, sets the weapon model to display, renders the model and updates the animation state.
void UpdateClassImageEntity(
const char* pModelName,
int x, int y, int width, int height )
{
C_BasePlayer* pLocalPlayer = C_BasePlayer::GetLocalPlayer();
if ( !pLocalPlayer )
return;
These next two declarations should be replaced with your own models / animations - you could also rework the image panels applysettings function to search for entries in the .res file to use here instead.
// set the weapon model and upper animation to use
const char* pWeaponName = models/weapons/f2000/w_f2000.mdl;
const char* pWeaponSequence = Idle_Upper_Aug;
C_BaseAnimatingOverlay* pPlayerModel = g_ClassImagePlayer.Get();
// Does the entity even exist yet?
bool recreatePlayer = ShouldRecreateClassImageEntity( pPlayerModel, pModelName );
If the above check in ShouldRecreateClassImageEntity returns true we need to setup our model with basic animation information, at this point we can get the model to move in any direction
if ( recreatePlayer )
{
// if the pointer already exists, remove it as we create a new one.
if ( pPlayerModel )
pPlayerModel->Remove();
// create a new instance
pPlayerModel = new C_BaseAnimatingOverlay();
pPlayerModel->InitializeAsClientEntity( pModelName, RENDER_GROUP_OPAQUE_ENTITY );
pPlayerModel->AddEffects( EF_NODRAW ); // don't let the renderer draw the model normally
At this point, we dictate which animation to use for the lower half of the body, I've specified a neutral idle animation here. SetPoseParameter takes the pose parameter to modify (as seen in the comments) and then the amount to alter the parameter by. If you want the model to walk, set move_x to 1.0f (or higher). HL2DM-based animations don't have an upper / lower animation set, instead they have an overarching animation, such as idle_pistol which can be blended with other animations like range_pistol.
// have the player stand idle
pPlayerModel->SetSequence( pPlayerModel->LookupSequence( Idle_lower ) );
pPlayerModel->SetPoseParameter( 0, 0.0f ); // move_yaw
pPlayerModel->SetPoseParameter( 1, 10.0f ); // body_pitch, look down a bit
pPlayerModel->SetPoseParameter( 2, 0.0f ); // body_yaw
pPlayerModel->SetPoseParameter( 3, 0.0f ); // move_y
pPlayerModel->SetPoseParameter( 4, 0.0f ); // move_x
g_ClassImagePlayer = pPlayerModel;
}
We now setup our weapon model, we check if we need to recreate it or if we recreated the player model - if we do need to recreate it, we create a new instance and set it up to follow the player model entity (so it appears correctly in the hands).
C_BaseAnimating* pWeaponModel = g_ClassImageWeapon.Get();
// Does the entity even exist yet?
if ( recreatePlayer || ShouldRecreateClassImageEntity( pWeaponModel, pWeaponName ) )
{
if ( pWeaponModel )
pWeaponModel->Remove();
pWeaponModel = new C_BaseAnimating();
pWeaponModel->InitializeAsClientEntity( pWeaponName, RENDER_GROUP_OPAQUE_ENTITY );
pWeaponModel->AddEffects( EF_NODRAW ); // don't let the renderer draw the model normally
pWeaponModel->FollowEntity( pPlayerModel ); // attach to player model
g_ClassImageWeapon = pWeaponModel;
}
We have to generate a light to use for illuminating the player model
Vector origin = pLocalPlayer->EyePosition();
Vector lightOrigin = origin;
// find a spot inside the world for the dlight's origin, or it won't illuminate the model
Vector testPos( origin.x - 100, origin.y, origin.z + 100 );
trace_t tr;
UTIL_TraceLine( origin, testPos, MASK_OPAQUE, pLocalPlayer, COLLISION_GROUP_NONE, &tr );
if ( tr.fraction == 1.0f )
lightOrigin = tr.endpos;
else
{
// Now move the model away so we get the correct illumination
lightOrigin = tr.endpos + Vector( 1, 0, -1 ); // pull out from the solid
Vector start = lightOrigin;
Vector end = lightOrigin + Vector( 100, 0, -100 );
UTIL_TraceLine( start, end, MASK_OPAQUE, pLocalPlayer, COLLISION_GROUP_NONE, &tr );
origin = tr.endpos;
}
float ambient = engine->GetLightForPoint( origin, true ).Length();
// Make a light so the model is well lit.
// use a non-zero number so we cannibalize ourselves next frame
dlight_t* dl = effects->CL_AllocDlight( LIGHT_INDEX_TE_DYNAMIC+1 );
dl->flags = DLIGHT_NO_WORLD_ILLUMINATION;
dl->origin = lightOrigin;
// Go away immediately so it doesn't light the world too.
dl->die = gpGlobals->curtime + 0.1f;
dl->color.r = dl->color.g = dl->color.b = 250;
if ( ambient < 1.0f )
dl->color.exponent = 1 + (1 - ambient)* 2;
dl->radius = 400;
With the light setup we now need to setup the player model by moving it in front of our view and setting up the animation to blend between our upper and lower sets. (in the case of HL2DM, there's no real need to blend unless you want a reload / fire animation to be playing as well)
// move player model in front of our view
pPlayerModel->SetAbsOrigin( origin );
pPlayerModel->SetAbsAngles( QAngle( 0, 210, 0 ) );
// set upper body animation
pPlayerModel->m_SequenceTransitioner.Update(
pPlayerModel->GetModelPtr(),
pPlayerModel->LookupSequence( idle_lower ),
pPlayerModel->GetCycle(),
pPlayerModel->GetPlaybackRate(),
gpGlobals->realtime,
false,
true
);
// Now, blend the lower and upper (aim) anims together
pPlayerModel->SetNumAnimOverlays( 2 );
int numOverlays = pPlayerModel->GetNumAnimOverlays();
for ( int i=0; i < numOverlays; ++i )
{
C_AnimationLayer* layer = pPlayerModel->GetAnimOverlay( i );
layer->flCycle = pPlayerModel->GetCycle();
if ( i )
layer->nSequence = pPlayerModel->LookupSequence( pWeaponSequence );
else
layer->nSequence = pPlayerModel->LookupSequence( walk_lower );
layer->flPlaybackrate = 1.0;
layer->flWeight = 1.0f;
layer->SetOrder( i );
}
pPlayerModel->FrameAdvance( gpGlobals->frametime );
Finally we create an area to draw the model on using basic information from the .res entry and player model itself.
// Now draw it.
CViewSetup view;
// setup the views location, size and fov (amongst others)
view.x = x;
view.y = y;
view.width = width;
view.height = height;
view.m_bOrtho = false;
view.fov = 54;
view.origin = origin + Vector( -110, -5, -5 );
// make sure that we see all of the player model
Vector vMins, vMaxs;
pPlayerModel->C_BaseAnimating::GetRenderBounds( vMins, vMaxs );
view.origin.z += ( vMins.z + vMaxs.z )* 0.55f;
view.angles.Init();
view.m_vUnreflectedOrigin = view.origin;
view.zNear = VIEW_NEARZ;
view.zFar = 1000;
view.m_bForceAspectRatio1To1 = false;
// render it out to the new CViewSetup area
// it's possible that ViewSetup3D will be replaced in future code releases
Frustum dummyFrustum;
// New Function instead of ViewSetup3D...
render->Push3DView( view, 0, NULL, dummyFrustum );
pPlayerModel->DrawModel( STUDIO_RENDER );
if ( pWeaponModel )
pWeaponModel->DrawModel( STUDIO_RENDER );
render->PopView( dummyFrustum );
}
PostRenderVGUI
This checks to see if any of the panels on our global list are visible, if they are, we run UpdateClassImageEntity() to draw the contents of the panel. Right now, it's setup to only draw a single image panel at a time - but remove the return call to have it continue looping through the list.
void ClientModeSDKNormal::PostRenderVGui()
{
// If the team menu is up, then render the model
for ( int i=0; i < g_ClassImagePanels.Count(); i++ )
{
CClassImagePanel* pPanel = g_ClassImagePanels[i];
if ( WillPanelBeVisible( pPanel->GetVPanel() ) )
{
// Ok, we have a visible class image panel.
int x, y, w, h;
pPanel->GetBounds( x, y, w, h );
pPanel->LocalToScreen( x, y );
// Allow for the border.
x += 3;
y += 5;
w -= 2;
h -= 10;
UpdateClassImageEntity( g_ClassImagePanels[i]->m_ModelName, x, y, w, h );
return;
}
}
}
A standard .res entry, the only addition is the 3DModel entry which specifies the player model to render. If you wish to be able to specify the weapon model or animation to use than new entries would be required here that have the same name as those looked at in CClassImagePanel::ApplySettings
classimage
{
ControlName ClassImagePanel
fieldName classimage
xpos 270
ypos 170
wide 512
tall 384
autoResize 0
pinCorner 0
visible 1
enabled 1
textAlignment west
3DModel models/player/iris.mdl
scaleImage 1
zpos 1
}