Difference between revisions of "Models on VGUI Panels"

From Valve Developer Community
Jump to: navigation, search
m (Added page to proper Categorys)
Line 317: Line 317:
         "zpos" "1"
         "zpos" "1"

Revision as of 02:57, 9 July 2006

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.


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
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 ) { Q_strncpy( m_ModelName, pName, sizeof( m_ModelName ) ); }
BaseClass::ApplySettings( inResourceData ); }
void CClassImagePanel::Paint() { BaseClass::Paint(); }
Panel *TeamMenu::CreateControlByName(const char *controlName) { if ( Q_stricmp( controlName, "ClassImagePanel" ) == 0 ) { return new CClassImagePanel( NULL, controlName ); }
return BaseClass::CreateControlByName( controlName ); }


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( Q_stricmp( pName, pNewModelName ) != 0 ); }


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 )
// 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; render->ViewSetup3D( &view, dummyFrustum );
pPlayerModel->DrawModel( STUDIO_RENDER );
if ( pWeaponModel ) { pWeaponModel->DrawModel( STUDIO_RENDER ); } }


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

        "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"