Animating to wind direction (ASM tweak)

From FSDeveloper Wiki
Jump to: navigation, search

When we want an object to rotate to the wind, we tweak the source code so that an additional rotation is added depending on the variable that contains the wind direction. But this kind of tweak does not work for parts that have been animated in GMax. This article describes a different way of tweaking the object that does work with animated parts as well.

The technique described here is based on the technique that is used by CAT to condition animations. But in this tutorial we will do much more, instead of the time we will use the wind direction to drive the animation. Let's get started!

Preparing for the tweak in GMax

To make the source tweaking a little bit easier, you will have to prepare your object a little bit in GMax. I used a model of a windmill (typical Dutch object) to test this technique. In my GMax object I created an animation of the vanes. This was all done in the normal way.

The next step was to add an additional dummy animation for the rotation of the head of the windmill. It does not really matter what this animation does, as we will tweak it later on. I just added a 90 degrees swing to the head. As the other animations, like the vanes, are linked to the rotation of the head, it might look a bit strange in GMax at the moment. But you don't have to worry about that. Don't forget the tick18_ prefix for this dummy object, else the correct code is not generated in the ASM file.

Once you are ready with your animations you can export the MDL file. Make sure that you keep the ASM source files as well, as you will need these to tweak the animation.

Tweaking the ASM file

The basic idea

The basic idea of the tweak we are going to do, is that instead of using the time to determine a frame of the animation table, we are going to use the direction of the wind to do this selection. So the animation table of for example 0 to 100 frames, corresponds with a wind direction between 0 and 360 degrees. To achieve this we will have to change a few things. The first is the animation table itself of course, it must represent a nice rotation between 0 and 360 degrees of the object. But we will also have to change the part of the code that now interpolates the frame number from a timer. This must read the wind direction for us and provide a frame number to use from that.

The original ASM code

Before we start tweaking it is probably useful to show you how the original ASM code looked at my machine. For your animation it could be different of course, but the structure of the code should be the same. Below you find the code of the ANIP section for the dummy animation (I have removed some of the frames from the table to keep it readable):

; Animation parameters for animation: 0
molen_zeldenrust_zuidbarge_1_tick18_mod_1 label word
    dw  42
    dw -32768,0,-29554,3215
    dw -29552,0,-26338,3215
    dw -26336,0,-23122,3215
    dw -23120,0,-19906,3215
    dw -19904,0,-16690,3215
    dw -16688,0,-13474,3215
    dw -13472,0,-10258,3215
    dw -10256,0,-7042,3215
    dw -7040,0,-3826,3215
    dw -3824,0,-610,3215
    dw -608,0,2606,3215
    dw 2608,0,5822,3215
    dw 5824,0,9038,3215
    dw 9040,0,12254,3215
    dw 12256,0,15470,3215
    dw 15472,0,18686,3215
    dw 18688,0,21902,3215
    dw 21904,0,25118,3215
    dw 25120,0,28334,3215
    dw 28336,0,31550,3215
    dw 31552,0,32767,1216
molen_zeldenrust_zuidbarge_1_tick18_mod_2 label word
    dw  32
    dw 0,0,200,200
    dw 201,0,401,200
    dw 402,0,602,200
    dw 603,0,803,200
    dw 804,0,1004,200
    dw 1005,0,1205,200
    dw 1206,0,1406,200
    dw 1407,0,1607,200
    dw 1608,0,1808,200
    dw 1809,0,2009,200
    dw 2010,0,2210,200
    dw 2211,0,2411,200
    dw 2412,0,2612,200
    dw 2613,0,2813,200
    dw 2814,0,3014,200
    dw 3015,0,3215,200
molen_zeldenrust_zuidbarge_1_float_convert_hi  label word
    dw  12               ; num entries
    dw  0,0
    dw  1,16256
    dw  2,16384
    dw  4,16512
    dw  8,16640
    dw  16,16768
    dw  32,16896
    dw  64,17024
    dw  128,17152
    dw  256,17280
    dw  512,17408
    dw  1024,17536
molen_zeldenrust_zuidbarge_quat_1        label word
    dw           3       ; 3: Quaternion (rotation) 
    real4     -1.0       ; Previous panim value
    real4 16 dup (0.0)   ; Cached matrix 
    dw         101       ; number of entries
    real4      0.0,  0.000000,  0.000000,  0.000000,  1.000000 ; frame/x/y/z/w values
    real4      2.0,  0.000000,  0.000000, -0.007854,  0.999969 ; frame/x/y/z/w values
    real4      4.0,  0.000000,  0.000000, -0.015707,  0.999877 ; frame/x/y/z/w values
    ;
    ; few frames removed here
    ;
    real4    196.0,  0.000000,  0.000000, -0.695913,  0.718126 ; frame/x/y/z/w values
    real4    198.0,  0.000000,  0.000000, -0.701531,  0.712638 ; frame/x/y/z/w values
    real4    200.0,  0.000000,  0.000000, -0.707107,  0.707107 ; frame/x/y/z/w values

And here is the original code of the ANIC section for the dummy animation:

; Animation command for animation: 0  output index: 1
bgl_animation_command_start_molen_zeldenrust_zuidbarge_1	label	BGLCODE
    ; usrvar = tick18 mod 201
    LOCAL_BASE_32 molen_zeldenrust_zuidbarge_1_tick18_mod_1
    BGL_INTERPOLATE VAR_BASE_GLOBAL,BGL_TICK18,\
                    VAR_BASE_GLOBAL,usrvar,\
                    VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_1_tick18_mod_1 - offset molen_zeldenrust_zuidbarge_1_tick18_mod_1)
    LOCAL_BASE_32 molen_zeldenrust_zuidbarge_1_tick18_mod_2
    BGL_INTERPOLATE VAR_BASE_GLOBAL,usrvar,\
                    VAR_BASE_GLOBAL,usrvar,\
                    VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_1_tick18_mod_2 - offset molen_zeldenrust_zuidbarge_1_tick18_mod_2)
    ; convert usrvar to float and store in usrvr2-usrvr3
    LOCAL_BASE_32 molen_zeldenrust_zuidbarge_1_float_convert_hi
    BGL_INTERPOLATE VAR_BASE_GLOBAL,usrvar,\
                    VAR_BASE_GLOBAL,usrvr3,\
                    VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_1_float_convert_hi - molen_zeldenrust_zuidbarge_1_float_convert_hi)
    VAR_BASE_32   VAR_BASE_GLOBAL
    SETWRD usrvr2,0
    LOCAL_BASE_32 molen_zeldenrust_zuidbarge_quat_1
    BGL_ANIMATE_INDIRECT   VAR_BASE_GLOBAL,usrvr2,VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_quat_1 - offset molen_zeldenrust_zuidbarge_quat_1),0.000000,-1.246083,-17.500000,1
 bgl_animation_command_end_molen_zeldenrust_zuidbarge_1	label	BGLCODE

Adding the local variable space

Before we start with the actual tweaking, there is one thing I added. A local variable space. This is not really needed, but doing it has some advantages. By default GMax stores the result of the interpolation to determine the frame number in the global user variables. But we have only 5 of them, so I find it a bit wasteful to use them for something like this. By defining a local variable space, we get some private variables that only this object can read. These are perfect to store a frame number for this object in. And if you would tweak all your GMax animations, it could even give you back some of the global user variables to use for something useful.

To create the local variable space, I added this code at the beginning of the code of my dummy animation in the ANIP section:

molen_zeldenrust_zuidbarge_anim_localvar label word
    dw 0
    dw 0
    dw 0
    dw 0
    dw 0

As you can see it is just a label we can later refer to and I set 5 variables with a default value of 0. If we later initialize the local variable space by referring to the label, these variables have the addresses of 0h, 2h, 4h, 6h and 8h.

The animation table

The next part of the code to edit is the animation table itself. As I wrote above I used a dummy object that made a 90 degree turn. In the code this has resulted in a rather big animation table with 101 entries. For our 360 degree turn I just want to use 5 entries, for 0, 90, 180, 270 and 360 degrees. The linear interpolation can then determine the correct heading for that just fine. Below you see my new table, with the five entries.

molen_zeldenrust_zuidbarge_quat_1        label word
    dw           3       ; 3: Quaternion (rotation) 
    real4     -1.0       ; Previous panim value
    real4 16 dup (0.0)   ; Cached matrix 
    dw         5         ; number of entries
    real4      0.0,  0.000000,  0.000000,  0.000000,  1.000000 ; frame/x/y/z/w values
    real4     25.0,  0.000000,  0.000000,  0.707106,  0.707106 ; frame/x/y/z/w values
    real4     50.0,  0.000000,  0.000000,  1.000000,  0.000000 ; frame/x/y/z/w values
    real4     75.0,  0.000000,  0.000000,  0.707106, -0.707106 ; frame/x/y/z/w values
    real4    100.0,  0.000000,  0.000000,  0.000000, -1.000000 ; frame/x/y/z/w values

As you can see I have chosen to let the total range of the table be from 0 to 100. I could also have chosen to use for example 0 to 360, but keeping the maximum value below 256 makes the resulting code slightly easier, so therefore I used 100 as maximum.

The four parameters that actually define the rotation might look more complex to you. As is explained in the comments of the source code, this is because they are in quaternion notation. I'll try to explain what you can do with that notation for those who did not study mathematics. Basically the first three parameters define the direction of the normal and the fourth parameter then defines the rotation along this normal. But the four elements together are also normalized. The value of the last parameter w is given by:

w = cos( 0.5 * angle)

See the table below for the calculation of the parameter w for our 5 headings.

Heading 0.5 * Heading cos( 0.5 * Heading )
0 0 1.000000
90 45 0.707106
180 90 0.000000
270 135 -0.707106
360 180 -1.000000

As you can see we now have the values of w, as I used them in the interpolation table. But how to get the other three parameters? In this case that was rather easy as we want the rotation to be along the z axis. So that means that the x and y parameters are zero. The z value of our vector would then normally be 1.0, but in this case we have to normalize it with the w value. That means that the z value becomes zero for the headings of 0 and 360, as the w parameter already has the value of 1.0 there. For the heading of 180 degrees the w parameter is zero, which means that z must have the value of one. For the two remaining headings, the size of the parameter w is the square root of a half. To get a total vector length of 1.0, this means the z parameter must also be the square root of a half. So that is how I calculated the animation table as shown above.

I hope this section is sort of clear for people without a very big mathematics talent. I tried to explain it as simple as I could and therefore it is probably not 100% mathematically correct for those who have that talent.

The wind interpolation table

The next step is to alter the table that determines the interpolation of the variable driving the animation. By default the Tick18 timer variable is used for this and with two interpolation tables the value of this time is split to be in the range of zero to the number of frames for your animation (with a maximum of 1024 frames). But for this rotation we do not want a timer to drive it, we want to use the wind speed. The variable that contains the wind direction can take a value between -32768 and 32767. As we want to map this on a frame number between 0 and 100 we get the interpolation table as shown below.

The additional table below (the one named float_convert) can be kept as MakeMDL made it. This is used to transform your frame number into a float, so that it can be used in the real animation. I still don't understand this table 100%, but it works as it is.

molen_zeldenrust_zuidbarge_1_tick18_mod_1 label word
    dw  2
    dw -32768,0,32767,100
molen_zeldenrust_zuidbarge_1_float_convert_hi  label word
    dw  12               ; num entries
    dw  0,0
    dw  1,16256
    dw  2,16384
    dw  4,16512
    dw  8,16640
    dw  16,16768
    dw  32,16896
    dw  64,17024
    dw  128,17152
    dw  256,17280
    dw  512,17408
    dw  1024,17536

Please not that by defining it like this the frame 0 corresponds to -180 degrees and the frame of 100 to 180 degrees. As you can see in the animation table above, there the values run from 0 to 360 degrees. I had to do this for my particular object to make sure it rotated correct with the wind. This depends on how you drew your object in GMax. So you might want to change the order of the headings in the animation table if your object is rotation wrongly into the wind.

The actual interpolation

Now the final step is to combine all these new tables and let them create your new animation. To do this I tweaked the animation as it was by default. See the final code below.

; Animation command for animation: 0  output index: 1
bgl_animation_command_start_molen_zeldenrust_zuidbarge_1	label	BGLCODE
    LOCAL_BASE_32 molen_zeldenrust_zuidbarge_anim_localvar
    BGL_INTERPOLATE VAR_BASE_GLOBAL,0C74h,\
                    VAR_BASE_LOCAL,0h,\
                    VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_1_tick18_mod_1 - molen_zeldenrust_zuidbarge_anim_localvar)
    ; convert usrvar to float
    BGL_INTERPOLATE VAR_BASE_LOCAL,0h,\
                    VAR_BASE_LOCAL,4h,\
                    VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_1_float_convert_hi - molen_zeldenrust_zuidbarge_anim_localvar)
    SETWRD 2h,0
    BGL_ANIMATE_INDIRECT   VAR_BASE_LOCAL,2h,VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_quat_1 - molen_zeldenrust_zuidbarge_anim_localvar),0.000000,-1.246083,-17.500000,1
 bgl_animation_command_end_molen_zeldenrust_zuidbarge_1	label	BGLCODE

The first line of code after the label, set the variable space to be used to our newly created local variable space. The next interpolation command then interpolates the wind direction variable (0C74h) using our table (which is defined after the label molen_zeldenrust_zuidbarge_1_tick18_mod_1). The resulting frame number is stored in the local variable 0h.

The next interpolation makes a float out of our new frame number. The result is written to the local variable 4h. Please also note that I changed how the molen_zeldenrust_zuidbarge_1_float_convert_hi table is called. The address has to be changed, to be relative to the new local variables.

On the next line with the SETWRD command you just have to replace the usrvr2 with 2h. If you animation has more than 256 frames, this line will look different. The final animation command then calculates the actual animation. It is using the variable 2h to interpolate from the table molen_zeldenrust_zuidbarge_quat_1.

Total code example

Here is the total code sample of the section above together. The first part is the definition of the tables in the ANIP section:

; Animation parameters for animation: 0
molen_zeldenrust_zuidbarge_anim_localvar label word
    dw 0
    dw 0
    dw 0
    dw 0
    dw 0
molen_zeldenrust_zuidbarge_1_tick18_mod_1 label word
    dw  2
    dw -32768,0,32767,100
molen_zeldenrust_zuidbarge_1_float_convert_hi  label word
    dw  12               ; num entries
    dw  0,0
    dw  1,16256
    dw  2,16384
    dw  4,16512
    dw  8,16640
    dw  16,16768
    dw  32,16896
    dw  64,17024
    dw  128,17152
    dw  256,17280
    dw  512,17408
    dw  1024,17536
molen_zeldenrust_zuidbarge_quat_1        label word
    dw           3       ; 3: Quaternion (rotation) 
    real4     -1.0       ; Previous panim value
    real4 16 dup (0.0)   ; Cached matrix 
    dw         5         ; number of entries
    real4      0.0,  0.000000,  0.000000,  0.000000,  1.000000 ; frame/x/y/z/w values
    real4     25.0,  0.000000,  0.000000,  0.707106,  0.707106 ; frame/x/y/z/w values
    real4     50.0,  0.000000,  0.000000,  1.000000,  0.000000 ; frame/x/y/z/w values
    real4     75.0,  0.000000,  0.000000,  0.707106, -0.707106 ; frame/x/y/z/w values
    real4    100.0,  0.000000,  0.000000,  0.000000, -1.000000 ; frame/x/y/z/w values

The second part is the interpolation of the animation itself in the ANIC section:

; Animation command for animation: 0  output index: 1
bgl_animation_command_start_molen_zeldenrust_zuidbarge_1	label	BGLCODE
    ; usrvar = tick18 mod 201
    LOCAL_BASE_32 molen_zeldenrust_zuidbarge_anim_localvar
    BGL_INTERPOLATE VAR_BASE_GLOBAL,0C74h,\
                    VAR_BASE_LOCAL,0h,\
                    VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_1_tick18_mod_1 - molen_zeldenrust_zuidbarge_anim_localvar)
    ; convert usrvar to float and store in usrvr2-usrvr3
    BGL_INTERPOLATE VAR_BASE_LOCAL,0h,\
                    VAR_BASE_LOCAL,4h,\
                    VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_1_float_convert_hi - molen_zeldenrust_zuidbarge_anim_localvar)
    SETWRD 2h,0
    BGL_ANIMATE_INDIRECT   VAR_BASE_LOCAL,2h,VAR_BASE_LOCAL,(offset molen_zeldenrust_zuidbarge_quat_1 - molen_zeldenrust_zuidbarge_anim_localvar),0.000000,-1.246083,-17.500000,1
 bgl_animation_command_end_molen_zeldenrust_zuidbarge_1	label	BGLCODE

Conclusions

After you have added the changes as described above, you can recompile the ASM source files into a MDL object. You can then use this object like any other object. Please note that you will have to place two copies of your object on your screen, else the rotation does not work. This is a know bug of the scenery engine.