Models on VGUI Panels

From Valve Developer Community
Jump to navigation Jump to search
Wikipedia - Letter.png
This article has multiple issues. Please help improve it or discuss these issues on the talk page. (Learn how and when to remove these template messages)
Dead End - Icon.png
This article has no Wikipedia icon links to other VDC articles. Please help improve this article by adding links Wikipedia icon that are relevant to the context within the existing text.
January 2024
Player model rendered in VGUI panel

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.

teammenu

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!

teammenu.h

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;

teammenu.cpp

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;
		}
	}
}

teammenu.res

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
}