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

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)

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.

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.

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 visibleboolWillPanelBeVisible(vgui::VPANELhPanel){while(hPanel){if(!vgui::ipanel()->IsVisible(hPanel))returnfalse;hPanel=vgui::ipanel()->GetParent(hPanel);}returntrue;}

Utility function to check if we should recreate the model data

// Called to see if we should be creating or recreating the model instancesboolShouldRecreateClassImageEntity(C_BaseAnimating*pEnt,constchar*pNewModelName){if(!pNewModelName||!pNewModelName[0])returnfalse;if(!pEnt)returntrue;constmodel_t*pModel=pEnt->GetModel();if(!pModel)returntrue;constchar*pName=modelinfo->GetModelName(pModel);if(!pName)returntrue;// reload only if names are differentreturn(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.

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 useconstchar*pWeaponName=models/weapons/f2000/w_f2000.mdl;constchar*pWeaponSequence=Idle_Upper_Aug;C_BaseAnimatingOverlay*pPlayerModel=g_ClassImagePlayer.Get();// Does the entity even exist yet?boolrecreatePlayer=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 instancepPlayerModel=newC_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 idlepPlayerModel->SetSequence(pPlayerModel->LookupSequence(Idle_lower));pPlayerModel->SetPoseParameter(0,0.0f);// move_yawpPlayerModel->SetPoseParameter(1,10.0f);// body_pitch, look down a bitpPlayerModel->SetPoseParameter(2,0.0f);// body_yawpPlayerModel->SetPoseParameter(3,0.0f);// move_ypPlayerModel->SetPoseParameter(4,0.0f);// move_xg_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=newC_BaseAnimating();pWeaponModel->InitializeAsClientEntity(pWeaponName,RENDER_GROUP_OPAQUE_ENTITY);pWeaponModel->AddEffects(EF_NODRAW);// don't let the renderer draw the model normallypWeaponModel->FollowEntity(pPlayerModel);// attach to player modelg_ClassImageWeapon=pWeaponModel;}

We have to generate a light to use for illuminating the player model

Vectororigin=pLocalPlayer->EyePosition();VectorlightOrigin=origin;// find a spot inside the world for the dlight's origin, or it won't illuminate the modelVectortestPos(origin.x-100,origin.y,origin.z+100);trace_ttr;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 illuminationlightOrigin=tr.endpos+Vector(1,0,-1);// pull out from the solidVectorstart=lightOrigin;Vectorend=lightOrigin+Vector(100,0,-100);UTIL_TraceLine(start,end,MASK_OPAQUE,pLocalPlayer,COLLISION_GROUP_NONE,&tr);origin=tr.endpos;}floatambient=engine->GetLightForPoint(origin,true).Length();// Make a light so the model is well lit.// use a non-zero number so we cannibalize ourselves next framedlight_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)

Finally we create an area to draw the model on using basic information from the .res entry and player model itself.

// Now draw it.CViewSetupview;// 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 modelVectorvMins,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 releasesFrustumdummyFrustum;// 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.

voidClientModeSDKNormal::PostRenderVGui(){// If the team menu is up, then render the modelfor(inti=0;i<g_ClassImagePanels.Count();i++){CClassImagePanel*pPanel=g_ClassImagePanels[i];if(WillPanelBeVisible(pPanel->GetVPanel())){// Ok, we have a visible class image panel.intx,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