How to animate a model – ArmA: Armed Assault
Attention: This article is a tutorial on how to animate parts of your model (e.g. wheels, rotors, etc.). Take all information of this article with a pinch of salt: everything is based on experiments and has not been confirmed nor documented by BIS, yet.
To animate a model, you have to make use of both, the cfgModels and cfgSkeletons class.
The cfgSkeletons class defines the bones of a vehicle. Bones are, more or less, the animated selections of a model.
The cfgModels class is an extended version of the OFP cfgModels class. It defines the selections of a model which you want to animate or use with the setObjectTexture command, but since ArmA, you have to put everything related to animate your model in here.
Model preparation: Besides adding selections (same as in OFP but in ArmA bone selections should never overlap) you also need to add a following named property to your first resolution LOD (press Alt + P to open named properties window):
property name: autocenter value: 0
If your model has Geometry LOD the above needs to be also done for your Geometry LOD. This property stopps engine from shifting the animation axes.
If you encounter a bug when your weapon bones fall apart after you dropp it to the ground the reason might be lack of Geometry or View - pilot LODs.
model.cfg
According to the article about Model Config, the cfgSkeletons and cfgModels class should be part of a model.cfg file which is located in the addon pbo file.
However, if you use the model.cfg file, you always need to binarize your addon - otherwise, your animations won't work.
Another option is to add both classes (cfgSkeletons and cfgModels) to the config.cpp file of your addon.
This way, you do not need to binarize your addon, so it is a perfect way to test it.
However, the first method is prefered and should always be used when releasing your addon.
cfgSkeletons
The cfgSkeletons class defines, as mentioned before, the bones (= animated selections) of a vehicle.
Each skeleton is a subclass within the cfgSkeletons class, consisting of three parameters:
Parameter | Description |
---|---|
isDiscrete | currently unknown, set to 1. |
skeletonInherit | inherit bones from given class. |
skeletonBones[] | define your own bones here. |
Defining a bone
Bones are defined in the skeletonBones[]-array which is made of a list of unsorted bones. Each bone is the name of a selection you want to animate.
A single bone
A bone is defined by using two strings:
- "bone", ""
You may define multiple bones by strining them togeter.
Attention: Do not leave out the second string as this will lead to errors! (see linked bones for more information on the second string argument)
Example
skeletonBones[]=
{
"bone1", "", // defines bone1
"bone2", "" // defines bone2
};
Linked bones
The second argument (empty in the example above) is used for linking two bones:
- "bone1", "bone2"
Linking is used to make the animation of "bone1" depending on the movement of "bone2". If you e.g. have a turret, you have to make use of linking here, because the up and down movement of the turrets weapon is typically influenced by left and right movement of the turret:
Example
skeletonBones[] =
{
"turret_x", "", // defines bone turret_x
"turret_y", "turret_x" // defines bone turret_y and makes it linked to bone turret_x
};
Attention:
You can not link more than two bones in a row!
If you do something like
"bone1", "bone2", "bone3"
this will result in an error, as Armed Assault interprets this as
- defining "bone1" which is linked to "bone2"
- defining "bone3", which misses the second argument.
However, it should be possible to use a syntax like this (not tested yet):
skeletonBones[]=
{
"bone1", "bone2", // defines bone "bone1" and makes it linked to "bone2"
"bone2", "bone3", // defines bone "bone2" and makes it linked to "bone3"
"bone3", "" // defines bone "bone3".
};
In conclusion, "bone1" is linked to "bone2", which is linked to "bone3".
So "bone1" should be depending on the movement of both, "bone2" and "bone3".
cfgSkeletons Example
class cfgSkeletons
{
class BWMod_Tiger_Skeleton
{
isDiscrete = 1;
skeletonInherit = "";
skeletonBones[] =
{
"wheelL", "",
"wheelR", "",
"turret_RMK_x", "",
"turret_RMK_y", "turret_RMK_x",
"turret_OSIRIS_y", "",
"turret_OSIRIS_x", "turret_OSIRIS_y"
};
};
};
cfgModels
The cfgModels class is used to declare the selections of a model you want to animate or access via the setObjectTexture command. Since ArmA, the cfgModels class has been extended an is now used to define all animations of a model.
Each of your model is a subclass inside of the cfgModels class and the models filename is used as the name of your class (without .p3d). E.g. your model p3d is named "myVehicle.p3d" your class is "class myvehicle {}". We will call them "modelclasses" in this article for the sake of simplicity.
Each modelclass consists of three parameters and an additional subclass which defines the animations of your models:
Parameter | Description |
---|---|
sectionsInherits | inherit sections (= selections) from given glass. |
sections[] | define your sections here. |
skeletonName | class name of skeleton used by this model. |
class Animations {} | subclass which defines the animations of your model. |
Sections
A section is the same as a selection: a part of the model which may be animated or changed via the setObjectTexture command. To define selections in your modelclass make use of the section[]-array. This array is an unordered list of all selections you want to use in the ways described above.
Example
sections[] =
{
"mainRotor", "mainRotor_static", "mainRotor_blur", "mainRotor_dive",
"tailRotor", "tailRotor_static", "tailRotor_blur", "tailRotor_dive",
"turret_RMK_x", "turret_RMK_y"
};
Animations
To define animations for your model, you have to make use of the class animations in your modelclass. Each animation is a subclass in the class animations with a userdefinable name and consists of the following parameters:
Parameter | Description |
---|---|
type | the type of the animation, e.g. rotating or translation. Refer to Model Config for a list of all animation types. |
source | The source used to animate the selection. Refer to Model Config for a list of all sources. |
selection | The name of the bone (= selection) you want to animate. Has to be defined in the cfgSkeletons class. |
axis | The name of the axis you want to use (only necessary for types rotation and translation). |
memory | If using an own axis (by the axis-parameter) use value 1 if axis is located in the memory lod of your model or 0 if the axis is located in the lod (or better: every lod) where your animated selection is used. |
sourceAdress | Use "loop" if you want your animation to "loop" (e.g. on wheels) or "clamp" if you want your animation to stop at a specific angle (e.g. on the steering wheel of a car). |
minValue | If source returns a value <= minValue, the animation is animated with angle0 (see below) |
maxValue | If source returns a value >= maxValue, the animation is animated with angle1 (see below) |
angle0 | The angle the selection is animated when minValue is reached. |
angle1 | The angle the selection is animated when maxValue is reached. |
offset0 | The distance the bone is moved when using type="Translation" and minValue is returned. Distance is calculated by the distance of axis' vertices. If the have a distance of 1m and you are using offset0 = 1, the bone will be moved by 1m. If using offset0=0.5, the bone will be moved by 0.5m, etc. |
offset1 | Same as offset0, but when maxValue is returned. |
Attention
I am assuming that each source is returning a value between 0 and 1 (the animationPhase) which is used to animate your selection. Some may even return a value from -1 to 1:
- If using
source = "speed"
, the source is returning 0 when the vehicle is not moving and 1 when the vehicle is moving at maximum speed. - If using
source = "drivingWheel"
, the source is returning -1 when the vehicle is turning left, 0 when the vehicle is not turning and 1 when the vehicle is turning right.
On turrets, it is necessary that the classnames of the animations match the according turret selection names.
So if you're mainTurret selection is named "turret_RMK_x" you have to name your class "turret_RMK_y", too.
The same goes for the mainGun selection.
Example
class mainRotor
{
type = "rotationY"; // rotation around the Y axis
source = "rotorH";
selection = "mainRotor";
axis = ""; // no own axis, use centre of selection
memory = 1;
sourceAddress = "loop";
minValue = 0;
maxValue = 1;
angle0 = 0;
angle1 = "rad -360";
};
Axes
Rules Of Engagement
cfgSkeletons
- Binarise gets confused if your cfgSkeletons classname is same as your cfgModels.
- Declare your skeleton class as MyModelSkeleton vs MyModel
- Similar to any other classes in any standard config.cpp of any pbo, it is possible, albeit less likely, to have same class(es) over declared in multiple folders. This is typical if you are running more than one project on the p: drive. Far better, you always use an OFPEC_TAG name
class OFPEC_TAG_MyModelSkeleton
- Declaring non-existent bones in a skeleton is 'ok'
Typically, you will have a base class skeleton (such as a 'vehicle') from which, you derive a tank.
Typically, all vehicles have number plates (vez, spz). Some, don't.
It is 'ok' to format for the general case. The only effect is that this non-existent bone will appear in the binary p3d as part of the skeleton list. No harm done.
cfgModels
- sections[] = have nothing to do with this document. They are a list of NamedSelections, from which, setObjectTexture can be used in-game. They are part of hiddenselections[]= and are, confusingly, not related to skeletons and animations. If a non-existent NamedSelection is, in fact, listed in this segment, it is ignored, and not present in the binary model.
- source= is the ANIMATION CLASS name. Often called 'controller'
- selection= is the BONE NAME in cfgSkeletons
- axis= is an axis. it is rarely declared as a bone, it rarely appears in sections[]=. It often appears in the NamedSelections list of the model.
- When creating class animation{... it is ok to declare non existent bones.
This is very prevalent, very common, and very 'ok' when you have a generalised base class that deals with four standard doors, only one of which appears in a *specific* model.
class WideDoor { //...
class FrontDoor { // ...
class BackDoor { // ...
class EmergencyExit{ // ...
Highly complex models such as aircraft frequently have multiple compass, rpm, altimeter and other classes in their base classes which may, or may not be present in THE model.
If the bone is not in cfgSkeletons, the cfgModel class is entirely ignored during the binarisation process.
If the bone IS declared in the skeleton but doesn't actually exist in the model. The cfgModel class IS binarised (probably not what you want to happen).
User Animation
class Rotation; // very standard, most often defined in the first model.cfg of the p:tree
cfgModels
{
class InheritedFromSomething // most often defined in the first model.cfg of the p:tree
{
class Animations;
};
class PinkElephant : InheritedFromSomething // this is for the PinkElephant.p3d and no other
{
class Animations : Animations // bring in all the base class classes
{
class OFPEC_TAG_MyPinkElephant_SlidingDoor : Rotation
{
source = SomeClassName; // defined in config.cpp
animPeriod = 1;
selection = SomeBoneName;
axis = SomeAxis;
angle1 = rad 85;
};
};
};
};
config.cpp
class CfgVehicles
{
class SomeBaseClass
{
class AnimationSources
{
class UserActions; // maybe
};
};
class OFPEC_TAG_MyPinkElephant : SomeBaseClass
{
// ...
model = PinkElephant;
class AnimationSources : AnimationSources
{
class OFPEC_TAG_MyPinkElephant_SlidingDoor /// <<<<<THE name used in THE cfgModels for THIS p3d
{
source = "user";
animPeriod = 1;
initPhase = 0;
};
class UserActions : UserActions
{
class OFPEC_TAG_OpenDoor1 // ensure no possibilty of a clash with base names (if any)
{
displayName = "Open Sliding door";
onlyforplayer = "true";
position = "SomeAxis"; // THE axis declared in the model
radius = 18100;
condition = "this animationPhase ""OFPEC_TAG_MyPinkElephant_SlidingDoor"" < 0.5";
statement = "this animate [""OFPEC_TAG_MyPinkElephant_SlidingDoor"", 1]; this say ""OFPEC_TAG_ElephantOpenSound""";
};
class OFPEC_TAG_CloseDoor1 : OFPEC_TAG_OpenDoor1
{
displayName = "Close Sliding door";
condition = "this animationPhase ""OFPEC_TAG_MyPinkElephant_SlidingDoor"" >= 0.5";
statement = "this animate [""OFPEC_TAG_MyPinkElephant_SlidingDoor"", 0]; this say ""OFPEC_TAG_ElephantCloseSound""";
};
};
};
};
};
class CfgSounds
{
class OFPEC_TAG_ElephantOpenSound // <<< make sure no other class could possibly called this
{
name = "whatever";
sound[] = { "\some\stupid\hard\path\somesound.ogg", 1, 1 };
titles[] = {};
};
class OFPEC_TAG_ElephantCloseSound : OFPEC_TAG_ElephantOpenSound
{
name = "think of a number";
sound[] = { "\some\stupid\hard\path\someOthersound", 1, 1 }; // wss equivalent
};
};
cfgModels example
class cfgModels
{
class bwmod_tiger
{
sectionsInherit = "";
sections[]=
{
"mainRotor", "mainRotor_static", "mainRotor_blur", "mainRotor_dive",
"tailRotor", "tailRotor_static", "tailRotor_blur", "tailRotor_dive",
"turret_RMK_x", "turret_RMK_y"
};
skeletonName = "BWMod_Tiger_Skeleton";
class Animations
{
class mainRotor
{
type = "rotationY";
source = "rotorH";
selection = "mainRotor";
axis = "";
memory = 1;
sourceAddress = "loop";
minValue = 0;
maxValue = 1;
angle0 = 0;
angle1 = "rad -360";
};
class tailRotor
{
type = "rotationX";
source = "rotorV";
selection = "tailRotor";
axis = "";
memory = 1;
sourceAddress = "loop";
minValue = 0;
maxValue = 1;
angle0 = 0;
angle1 = "rad -360";
};
class wheelL
{
type = "translation";
source = "altRadar"; // using altRadar, since damper doesn't seem to work on
// helicopters, even though [[Model_Config]] states something else.
selection = "wheelL";
axis = "axis_damper"; // vertical axis, vertex distance 1 m
memory = 0;
animPeriod = 0;
minValue = 0;
maxValue = 0.05; // max value 0.05m above ground
offset0 = 0;
offset1=-0.05; // animate wheels downwards for 0.05m when maxValue is reached.
};
class wheelR
{
type = "translation";
source = "altRadar";
selection = "wheelR";
axis = "axis_damper";
memory = 0;
animPeriod = 0;
minValue = 0;
maxValue = 0.05;
offset0 = 0;
offset1=-0.05;
};
class turret_RMK_x // the horizontal moving part of the turret
{
type = "rotationY";
source = "mainTurret";
selection = "turret_RMK_x";
axis = "axis_turret_RMK_x";
animPeriod = 0;
memory = 1;
minValue = "rad -360";
maxValue = "rad +360";
angle0 = "rad -360";
angle1 = "rad +360";
};
class turret_RMK_y // the vertical moving part of the turret
{
type = "rotationX";
source = "mainGun";
selection = "turret_RMK_y";
axis = "axis_turret_RMK_y";
animPeriod = 0;
memory = 1;
minValue = "rad -360";
maxValue = "rad +360";
angle0 = "rad -360";
angle1 = "rad +360";
};
};
};
};