SFM/Authoring HWM
This is a tutorial for authoring high-end SFM heads, commonly refereed to as HWM. There's still a lot of work to be done here, but this will get a working head included as an example file in SFM into the engine with it's corrective shapes, wrinkle maps, and all.
Creating the Flexes
Creating the flexes is an interesting process. You've gotta make sure your artists won't have a pain and with HWM, it's all about giving the artists and animators control. There's not much that can be said in terms of actually creating the flexes. Rather, just try to match the many references Valve has given us. There's human references available in the Left 4 Dead 2 survivor's sources, which can be found in left 4 dead 2/sdk_content/Maya_Rigs_Animation/Survivors. There's also a reference of the Scout, available in the SFM, in SourceFilmmaker/game/sdktools/lua/reference_heads. For this tutorial, we'll be using the Scout reference.
Exporting the Model
If you're using Maya, you've gotta follow a couple steps to export the model correctly. With your model selected, go into the MEL script editor and enter this command
vsDmxIO -export -ufc -ac -exportType model -exportType skeletalAnimation -selection -filename "C:/Foo/Bar/parts/dmx/head_morphs.dmx"
Why do we use this command? Because of -ufc. Maya normally uses hyphens (-) in place of underscores (_), and underscores are required for dmxedit to recognize the blend shape as a corrector. -ufc means "Underscores For Correctives," which preserves underscores in the export.
Blender will preserve underscores for you, so you can skip this, open source users! Here's a script snippet for blender users that will change hyphens to underscores if there's a need. Select your mesh and copy-paste this in the console.
# To underscores
for s in C.active_object.data.shape_keys.key_blocks:
s.name = s.name.replace('-', '_')
Coding for DMXedit
dmxedit requires a .lua script that tells the program to run the files inside game/sdktools/lua and execute the commands in face_correctors.lua to build a library of HWM flexes from a modeled base. (for more information on creating HWM flexes from a base flex shape library see the dmxedit syntax) Under is a script used for the Medic in tf_movies, so this is a good jumping off point. For this tutorial, we will be using a modified game/sdktools/lua/reference_heads/tf_movies.dmx which has had it's correctives properly named with underscores instead of hyphens (someone at Valve, please export the scout's head with ufc!!)
You can download it here [[1]].
As for the script, this is what it looks like.
-- Set the game mod to be tf_movies - all actors live in the tf_movies mod
vs.SetGame( "usermod" );
-- load in the actor source file
Load( vs.ContentDir() .. "modelsrc/template/parts/dmx/head_morphs.dmx", "relative" );
base = "base";
-- define what the separator is for selection, as currently some of the select shapes are "SELECT-eyes" and some are "SELECT_eyes
selectSeparator = "-";
-- flag what parts of the face are going to be compiled
upperFaceSwitch = true ;
lowerFaceSwitch = true ;
-- lock teeth and throat socket.
if lowerFaceSwitch then
-- dofile( vs.GameDir() .. "../sdktools/lua/face_lockJaw.lua" );
end
-- generate corrective combinations.
dofile( vs.GameDir() .. "../sdktools/lua/face_correctors.lua" );
ResetState();
SaveDelta( "TongueBack" );
SaveDelta( "TongueCurlDown" );
SaveDelta( "TongueCurlUp" );
SaveDelta( "TongueFunnel" );
SaveDelta( "TongueLeft" );
SaveDelta( "TongueNarrow" );
SaveDelta( "TongueOut" );
SaveDelta( "TongueRight" );
SaveDelta( "TongueV" );
SaveDelta( "TongueWide" );
SaveDelta( "SELECT-tongue" );
-- group eye controls
GroupControls( "CloseLid", "CloseLidLo", "CloseLidUp" );
GroupControls( "BrowInV", "WrinkleNose", "RaiseBrowIn" );
GroupControls( "NoseV", "PressNose", "SneerNose" );
GroupControls( "NostrilFlare", "SuckNostril", "BlowNostril" );
GroupControls( "CheekH", "DeflateCheek", "InflateCheek" );
GroupControls( "JawD", "SuckJaw", "JutJaw" );
GroupControls( "JawH", "SlideJawR", "SlideJawL" );
GroupControls( "JawV", "ClenchJaw", "OpenJaw" );
GroupControls( "LipsV", "CompressLips", "OpenLips" );
GroupControls( "LipUpV", "JutUpperLip", "OpenUpperLip" );
GroupControls( "LipLoV", "RaiseChin", "OpenLowerLip" );
GroupControls( "Smile", "SmileFlat", "SmileFull", "SmileSharp" );
GroupControls( "FoldLipUp", "SuckLipUp", "FunnelLipUp" );
GroupControls( "FoldLipLo", "SuckLipLo", "FunnelLipLo" );
GroupControls( "ScalpD", "ScalpBack", "ScalpForward" );
GroupControls( "TongueH", "TongueLeft", "TongueRight" );
GroupControls( "TongueCurl", "TongueCurlUp", "TongueCurlDown" );
GroupControls( "TongueD", "TongueBack", "TongueOut" );
GroupControls( "TongueWidth", "TongueNarrow", "TongueWide" );
-- reorder controls
ReorderControls(
"CloseLid",
"InnerSquint",
"OuterSquint",
"BrowInV",
"BrowOutV",
"Frown",
"NoseV",
"NostrilFlare",
"CheekV",
"CheekH",
"JawD",
"JawH",
"JawV",
"LipsV",
"LipUpV",
"LipLoV",
"Smile",
"Platysmus",
"FoldLipUp",
"FoldLipLo",
"PuckerLipUp",
"PuckerLipLo",
"LipCnrTwst",
"Dimple",
"PuffLipUp",
"PuffLipLo",
"ScalpD",
"TongueD",
"TongueH",
"TongueV",
"TongueCurl",
"TongueFunnel",
"TongueWidth"
);
SetEyelidControl("CloseLid", true );
-- setup stereo controls
SetStereoControl("CloseLid", true );
SetStereoControl("InnerSquint", true );
SetStereoControl("OuterSquint", true );
SetStereoControl("BrowInV", true );
SetStereoControl("BrowOutV", true );
SetStereoControl("Frown", true );
SetStereoControl("NoseV", true );
SetStereoControl("NostrilFlare", true );
SetStereoControl("CheekV", true );
SetStereoControl("CheekH", true );
SetStereoControl("JawD", false );
SetStereoControl("JawH", false );
SetStereoControl("JawV", false );
SetStereoControl("LipsV", true );
SetStereoControl("LipUpV", true );
SetStereoControl("LipLoV", true );
SetStereoControl("Smile", true );
SetStereoControl("Platysmus", true );
SetStereoControl("FoldLipUp", true );
SetStereoControl("FoldLipLo", true );
SetStereoControl("PuckerLipUp", true );
SetStereoControl("PuckerLipLo", true );
SetStereoControl("LipCnrTwst", true );
SetStereoControl("Dimple", true );
SetStereoControl("PuffLipUp", true );
SetStereoControl("PuffLipLo", true );
SetStereoControl("ScalpD", true );
SetStereoControl("TongueV", false );
SetStereoControl("TongueH", false );
SetStereoControl("TongueCurl", false );
SetStereoControl("TongueD", false );
-- add control dominators
AddDominationRule( { "BrowOutV" }, { "WrinkleNose"} );
AddDominationRule( { "FunnelLipLo" }, { "PuffLipLo"} );
AddDominationRule( { "FunnelLipLo" }, { "PuffLipUp"} );
AddDominationRule( { "FunnelLipUp" }, { "PuffLipLo"} );
AddDominationRule( { "FunnelLipUp" }, { "PuffLipUp"} );
AddDominationRule( { "LipCnrTwst" }, { "Dimple"} );
AddDominationRule( { "OpenJaw" }, { "InflateCheek"} );
AddDominationRule( { "OpenLips" }, { "PuffLipLo"} );
AddDominationRule( { "OpenLips" }, { "PuffLipUp"} );
AddDominationRule( { "OpenLowerLip" }, { "CompressLips"} );
AddDominationRule( { "OpenLowerLip" }, { "FunnelLipLo"} );
AddDominationRule( { "OpenLowerLip" }, { "PuffLipLo"} );
AddDominationRule( { "OpenLowerLip" }, { "PuffLipUp"} );
AddDominationRule( { "OpenLowerLip", "OpenUpperLip" }, { "OpenLips"} );
AddDominationRule( { "OpenUpperLip" }, { "CompressLips"} );
AddDominationRule( { "OpenUpperLip" }, { "FunnelLipUp"} );
AddDominationRule( { "OpenUpperLip" }, { "PuffLipLo"} );
AddDominationRule( { "OpenUpperLip" }, { "PuffLipUp"} );
AddDominationRule( { "Platysmus" }, { "FunnelLipLo"} );
AddDominationRule( { "Platysmus" }, { "FunnelLipUp"} );
AddDominationRule( { "Platysmus" }, { "LipCnrTwst"} );
AddDominationRule( { "Platysmus" }, { "PuckerLipLo"} );
AddDominationRule( { "Platysmus" }, { "PuckerLipUp"} );
AddDominationRule( { "PuckerLipLo" }, { "SmileFlat"} );
AddDominationRule( { "PuckerLipLo" }, { "SmileFull"} );
AddDominationRule( { "PuckerLipLo" }, { "SmileSharp"} );
AddDominationRule( { "PuckerLipLo" }, { "SuckLipLo"} );
AddDominationRule( { "PuckerLipLo", "OpenJaw" }, { "FunnelLipLo"} );
AddDominationRule( { "PuckerLipUp" }, { "SmileFlat"} );
AddDominationRule( { "PuckerLipUp" }, { "SmileFull"} );
AddDominationRule( { "PuckerLipUp" }, { "SmileSharp"} );
AddDominationRule( { "PuckerLipUp" }, { "SuckLipUp"} );
AddDominationRule( { "PuckerLipUp", "OpenJaw" }, { "FunnelLipUp"} );
AddDominationRule( { "SmileFull" }, { "InflateCheek"} );
AddDominationRule( { "SmileFull" }, { "SuckLipUp"} );
dofile( vs.GameDir() .. "../sdktools/lua/face_lipZipper.lua" );
Import( vs.ContentDir() .. "modelsrc/template/parts/dmx/teeth_sfm.dmx" );
dofile( vs.GameDir() .. "../sdktools/lua/face_wrinkleScales.lua" );
ComputeNormals();
ComputeWrinkles();
-- generate wrinkle weights maps for the teeth to stop them from glowing.
dofile( vs.GameDir() .. "../sdktools/lua/face_wrinkleTeeth.lua" );
-- create wrinkle deltas for glowing tongue
ResetState();
SetState( "SELECT-tongue" ) ;
ComputeWrinkle( "OpenJaw", 1 );
DeleteDelta( "SELECT-tongue" );
-- cleanup
dofile( vs.GameDir() .. "../sdktools/lua/face_cleanup.lua" );
-- Save a version of the head for the sfm.
Save( vs.ContentDir() .. "modelsrc/template/parts/dmx/head_morphs_sfm.dmx" );
Some commands you should take note of are the Load and Save commands which will load your exported version and save to a version read by StudioMDL. If you are just looking to enable correctors, your dmx just needs to run through DMXEdit through only the Load and Save commands, but if you're doing something as complex as the faces, it's best to stick with Valve's scripts.
Modify this code as you need to and save in your model's directory, inside a new folder next to paths called scripts, as your_model_name.lua.
If you want more on what all this does, run game/bin/dmxedit.exe with -help. It will give you a list of commands you can run in DMXedit. What this basically does is runs the .lua files in game/sdktools/lua, which will create correctors, generate wrinkle maps, and all that good stuff that makes HWM.
Now you have a script which will edit the .dmx and save it as head_morphs_sfm.dmx. However, we still need to merge this with the rest of the model. Due to some glitches in the system, we can't do this in the same script as our head. So what we will do is load in a second script which will merge head_morphs_sfm.dmx with modelname_model.dmx.
-- Set the game mod to be tf_movies - all actors live in the tf_movies mod
vs.SetGame( "usermod" );
-- load in the actor source file
Load( vs.ContentDir() .. "modelsrc/template/parts/dmx/head_morphs_sfm.dmx", "relative" );
Merge( vs.ContentDir() .. "modelsrc/template/parts/dmx/your_model_name_model.dmx", vs.ContentDir() .. "modelsrc/template/parts/dmx/your_model_name_model_sfm.dmx" );
Batch Compiling
To make the process easier, Bay showed us a nice way to compile the model and make it run through all it's needed programs after changes are made. Create a batch script in your models directory, next to the .qc, and name it compile_your_model_name.bat. Edit it so it reads:
%VGAME%/bin/dmxedit.exe -nop4 %VCONTENT%/usermod/modelsrc/template/scripts/your_model_name.lua
pause
%VGAME%/bin/studiomdl.exe %VCONTENT%/usermod/modelsrc/template/your_model_name_merge.qc
pause
%VGAME%/bin/studiomdl.exe %VCONTENT%/usermod/modelsrc/template/your_model_name.qc
pause
This basically runs the .dmx through dmxedit for ease and comfort. If you have any texture sources too, you could optionally run them all on the same file through vtex. Run the .bat file for dmx to run through all the required steps. At the end of it, you should have a model compiled and working!
Credits and TODO
There's still a ton of stuff that needs to be done for this, such as documenting left and right controllers and speedmaps, getting this to run on the Left 4 Dead 2 characters, etc. As it comes to us, we'll write documents about it. We should also document what each script does, so people creating custom characters will know what they're doing.
Credits go to:
Bay Raitt for the Medic Script plus general help
Narry Gewman for fixing the scripts for public use and general help
Cra0kalo for general help