Thread: Runescape's Animation System

Results 1 to 6 of 6
  1. #1 Runescape's Animation System 
    nice


    Join Date
    Jul 2014
    Posts
    708
    Thanks given
    341
    Thanks received
    472
    Discord
    View profile
    Rep Power
    3695
    This thread will explain how the Animation system works in Runescape(317 & OSRS to be more specific afaik it did not change much in higher revisions)

    I'm mainly making this thread because when i first wanted to understand how runescape's animation system works, i found no useful posts that described it in detail and the only resource was the client itself(altho a very good resource, it's just that back in early 2020 i didn't even really understand basic 3d rendering concepts so it was difficult to fully understand how the animation system worked)

    Rigging
    Before you can create animations for a model, it needs to be rigged.
    Generally rigging refers to the process of creating the bones(armature) and then every vertex in the model would be influenced by 1 or more bones, how much it is influenced depends on the weight of that vertex.
    However in runescape there's no concept of weights, instead each vertex in the model is labeled(just an id p much)

    Now, in the client before each animation is applied, the labels are grouped, for example if the model had the following labels:
    [5, 5, 2, 2, 7] the following groups would be generated: [[], [], [2, 3], [], [], [0, 1], [], [4]]
    In order to understand this better, lets look at the code in the client that creates the groups:
    Code:
        public void applyGroups() {
            if (vertexLabels != null) {
                int[] labelCount = new int[256];
                int topLabel = 0;
    
                for (int v = 0; v < vertexCount; v++) {
                    int label = vertexLabels[v];
                    labelCount[label]++;
    
                    if (label > topLabel) {
                        topLabel = label;
                    }
                }
    
                groupedLabels = new int[topLabel + 1][];
    
                for (int l = 0; l <= topLabel; l++) {
                    groupedLabels[l] = new int[labelCount[l]];
                    labelCount[l] = 0;
                }
    
                for (int v = 0; v < vertexCount; v++) {
                    int label = vertexLabels[v];
                    groupedLabels[label][labelCount[label]++] = v;
                }
            }
        }
    As you can see it first finds the largest label and then creates the 2d array where the outer array has it's length set to the value of the largest label + 1
    So for the example above [5, 5, 2, 2, 7] this would yield a 2d array where the length of the outer array is 8 as the largest label in the labels array is 7
    The inner array simply consists of the indices where that label occurs in the labels array
    So if we look at the result [[], [], [2, 3], [], [], [0, 1], [], [4]] again:
    The first array is for label 0, however the label array did not contain a value of 0 therefore it's just an empty array, the same for label 1
    the 3rd array which is for label 2 has [2, 3] because the label 2 occurred at indices 2 and 3 in the labels array
    the next 2 are empty again(as we did not have labels with the value of 3 or 4) however at index 5 we see [0, 1] again which is for label 5, it has an array with the values [0, 1] because the label 5 occurred at the first two indices and i think u can already see why the next one is empty and the last index has an array with the value 4 in it

    Okay, in case you still don't understand how the code above works(to be honest i did not fully understand it either when i first looked at it) there's a simpler way to write it
    We can have a Map<Integer, int[]> where the key is the label and the value is the indices where that label occurred in the labels array

    Code:
            Map<Integer, int[]> groupedLabels = new HashMap<>();
    
            for (int label : labels) {
                if (groupedLabels.containsKey(label)) {
                    continue;
                }
                int[] indices = IntStream.range(0, labels.length)
                        .filter(i -> labels[i] == label)
                        .toArray();
                groupedLabels.put(label, indices);
            }
    Animation/Sequence

    Alright lets now look at the data that an animation contains(NOTE: this is for 317 & OSRS, but as i mentioned earlier afaik this didn't change much in higher revisions)
    An animation consists of n frames where a frame contains the following data:
    Code:
    public class SeqFrame {
        int id;
        int[] xModifier;
        int[] yModifier;
        int[] zModifier;
        int[] groups;
        int groupCount;
        SeqTransform transform;
    }
    The important parts here are the 3 modifier arrays, the groups and the transform, you'll see how those are used later on

    Lets look at SeqTransform
    Code:
    public class SeqTransform {
        int id;
        int[] types;
        int[][] groupLabels;
        int length;
    }
    Again, the only important parts here are the types array and the group labels array

    finally, we have the animation/sequence definition, it contains a bunch of data but the important parts are:
    Code:
    int[] frameIDs;
    int[] delays;
    Why the mentioned are important & how they're used will become apparent in just a moment

    Now lets look at how animations are actually applied to models
    The animation code in the client basically does this:
    Code:
            for (int i = 0; i < frame.getGroupCount(); i++) {
                int group = frame.getGroups()[i];
                int type = frame.getTransform().getTypes()[group];
                int[] labels = frame.getTransform().getGroupLabels()[group];
                int x = frame.getXModifier()[i];
                int y = frame.getYModifier()[i];
                int z = frame.getZModifier()[i];
                animate(type, labels, x, y, z); // alternatively, this could be called transform
    }
    the type refers to the transform type used for that specific transformation, the types used in the rs client are:
    0(ORIGIN)
    1(TRANSLATION)
    2(ROTATION)
    3(SCALING)
    5(ALPHA)
    the labels as explained above simply store indices to the grouped labels array which contains arrays of vertex indices(or vertex groups)
    the x, y, z are the values that the model is going to be affected by(eg it is translated/rotated or scaled by x, y, z)
    Lets look at the animate function:
    Code:
        public void animate(int type, int[] labels, int dx, int dy, int dz) {
            switch (type) {
                case SET_ORIGIN -> setOrigin(labels, dx, dy, dz);
                case APPLY_TRANSLATION -> applyTranslation(labels, dx, dy, dz);
                case APPLY_ROTATION -> applyRotation(labels, dx, dy, dz);
                case APPLY_SCALING -> applyScaling(labels, dx, dy, dz);
                case APPLY_ALPHA -> applyAlpha(labels, dx);
            }
        }
    Now lets look at what each type does(translation, rotation, scaling, alpha are obvious) but i'll also explain how each of those work

    SET_ORIGIN:
    Code:
        private void setOrigin(int[] labels, int dx, int dy, int dz) {
            int displacedVertexCount = 0;
    
            resetOffset(); // set originX, originY, originZ to 0;
    
            for (int label : labels) {
                if (skeleton.getVertexGroup(label) == null) {
                    continue;
                }
                for (int vertex : skeleton.getVertexGroup(label)) {
                    originX += skeleton.getX(vertex);
                    originY += skeleton.getY(vertex);
                    originZ += skeleton.getZ(vertex);
                    displacedVertexCount++;
                }
            }
    
            if (displacedVertexCount > 0) {
                originX = dx + originX / displacedVertexCount;
                originY = dy + originY / displacedVertexCount;
                originZ = dz + originZ / displacedVertexCount;
            } else {
                originX = dx;
                originY = dy;
                originZ = dz;
            }
        }
    As you can see, it simply gets the midpoint of all the vertices inside the vertex groups which becomes the origin
    the origin of what u might wonder, of course the rotation origin because rotations happen around an origin(or about the origin whichever one makes more sense to you) therefore we need to define the rotation origin before each rotation(It is also why before every transform with TYPE_ROTATION a transform with TYPE_ORIGIN occurs) it simply sets the origin for the next transform

    Lets now look at what happens when the transform has the type TYPE_ROTATION
    Code:
        private void applyRotation(int[] labels, int dx, int dy, int dz) {
            for (int label : labels) {
                if (skeleton.getVertexGroup(label) == null) {
                    continue;
                }
                for (int vertex : skeleton.getVertexGroup(label)) {
                    skeleton.translate(vertex, -originX, -originY, -originZ);
                    if (dz != 0) skeleton.rotateXY(vertex, SINE[dz], COSINE[dz]); // roll
                    if (dx != 0) skeleton.rotateZY(vertex, SINE[dx], COSINE[dx]); // pitch
                    if (dy != 0) skeleton.rotateXZ(vertex, SINE[dy], COSINE[dy]); // yaw
                    skeleton.translate(vertex, +originX, +originY, +originZ);
                }
            }
        }
    First it moves the vertex to the coordinate system origin(0, 0, 0) then it rotates it and moves it back afterwards
    Lets look at the rotation methods:
    Code:
        public void rotateXY(int vertex, int sin, int cos) {
            int x = getX(vertex);
            int y = getY(vertex);
    
            int newX = y * sin + x * cos >> 16;
            int newY = y * cos - x * sin >> 16;
    
            setX(vertex, newX);
            setY(vertex, newY);
        }
    
    
        public void rotateZY(int vertex, int sin, int cos) {
            int z = getZ(vertex);
            int y = getY(vertex);
    
            int newZ = y * sin + z * cos >> 16;
            int newY = y * cos - z * sin >> 16;
    
            setZ(vertex, newZ);
            setY(vertex, newY);
    
        }
    
    
        public void rotateXZ(int vertex, int sin, int cos) {
            int x = getX(vertex);
            int z = getZ(vertex);
    
            int newX = z * sin + x * cos >> 16;
            int newZ = z * cos - x * sin >> 16;
    
            setX(vertex, newX);
            setZ(vertex, newZ);
        }
    The only thing im going to explain here is why the >> 16 is required as the rest you can find from just looking up 'rotation matrix' (altho you also have to take the coordinate system into account)
    So as you saw above, our rotation method uses a SINE, COSINE table instead of Math.sin and Math.cos
    That is because runescape(in 317 & osrs, in higher revs they changed it to 16384 afaik) uses 2048 degree rotations instead of 360, the tables are computed like this:
    Code:
        public static final double UNIT = Math.PI / 1024d; // How much of the circle each unit of SINE/COSINE is
    
        public static final int[] SINE = new int[2048]; // sine angles for each of the 2048 units, * 65536 and stored as an int
        public static final int[] COSINE = new int[2048]; // cosine
    
        static {
            for (int i = 0; i < 2048; ++i) {
                SINE[i] = (int) (65536.0D * Math.sin((double) i * UNIT));
                COSINE[i] = (int) (65536.0D * Math.cos((double) i * UNIT));
            }
        }
    as you can see each value is multiplied by 65536 which is why the >> 16 is necessary, NOTE: that shifting right by 16 is equal to dividing by 65536

    APPLY_TRANSLATION, this method is very simple it simply adds dx, dy, dz to the vertex

    APPLY_SCALING
    Once again a fairly simple method, however before it scales the vertex it moves it to the origin, scales it, moves it back afterwards:
    Code:
        private void applyScaling(int[] labels, int dx, int dy, int dz) {
            for (int label : labels) {
                if (skeleton.getVertexGroup(label) == null) {
                    continue;
                }
                for (int vertex : skeleton.getVertexGroup(label)) {
                    skeleton.translate(vertex, -originX, -originY, -originZ);
                    skeleton.scale(vertex, dx, dy, dz);
                    skeleton.translate(vertex, +originX, +originY, +originZ);
                }
            }
        }
    The last method for the APPLY_ALPHA type is also very simple:
    Code:
        private void applyAlpha(int[] labels, int dx) {
            for (int label : labels) {
                if (skeleton.getTriangleGroup(label) == null) {
                    continue;
                }
    
                for (int triangle : skeleton.getTriangleGroup(label)) {
                    skeleton.setAlpha(triangle, dx);
                }
            }
        }
    and all setAlpha does is add dx * 8 to the current alpha value of the triangle, if it's less than 0 afterwards then sets it to 0, if it's over 255 sets it to 255
    i also didn't mention triangle groups above, they're simply the triangle 'labels' grouped (the same exact way as vertex labels)

    Now, lets look at an example to have a better understanding of how labels are used

    Assume that i have a character model, that has the label '3' defined for all vertices of the shoulder and the label '4' for all vertices of the elbow and the label '5' for all vertices of the hand
    Now if i wanted to animate(transform) all of the 3(shoulder, elbow, hand) i would need to create a transformation with the labels [3, 4, 5] which would indicate that all the vertices labeled with 3, 4, 5 should be moved with this transformation

    Alright i think that's it for explaining the animation system, if you think i missed something or don't understand a certain part, feel free to ask and i can probably clarify

    I would also like to briefly explain how an editor for editing or creating new animations could be created
    So as i already explained, each vertex has a label which is used for animation and then those labels are grouped
    Possibility 1 would be to just create a tool for creating animations using those grouped labels, however that is not optimal for 2 reasons:
    1) More complicated models have a lot of different labels(usually 50+, the Vet'ion model for example has 59) therefore creating animations would take a while & it would be rather tedious
    2) Because most transformations are rotations, you would also need to define a rotation for each origin making it even more tedious

    However there's a much better solution, if you would like to make new animations(or edit existing animations) of a model that has already been animated before, you can simply look at the existing animations to build a hierarchy.
    This would be ideal because you would be able to transform labels that never transformed alone in that animation in groups, meaning you would have less labels but would still be able to animate each part of the mesh which would make it much much easier to make or edit animations, on top of that you would also know the origin for each rotation and wouldn't need to create your own.

    Building the hierarchy is very easy, one simply has to look at an existing animation and see which vertex groups always transform together, for example if during the whole animation the shoulder never moved alone but it moved together with all the labels of the elbow, then you can infer that the elbow is a children of shoulder, so if you move the shoulder the elbow should also move.

    I also thought about covering how Runescape's rigged models could be converted to a different format(like glTF, fbx or collada) so new animations could be created in programs like blender however that would be a very lengthy thread and i don't think it's really related to this thread so if i choose to do it, i'll make a new thread.
    Reply With Quote  
     


  2. #2  
    Drakops.com

    Kharyrll's Avatar
    Join Date
    Feb 2018
    Posts
    703
    Thanks given
    4
    Thanks received
    147
    Discord
    View profile
    Rep Power
    811
    As usual, amazing detail!
    Reply With Quote  
     

  3. Thankful user:


  4. #3  
    Donator

    Join Date
    May 2020
    Posts
    46
    Thanks given
    4
    Thanks received
    16
    Discord
    View profile
    Rep Power
    58
    might read when i have 3 days to spare

    thnx for this will use eventually
    Reply With Quote  
     

  5. #4  
    Respected Member


    Luke132's Avatar
    Join Date
    Dec 2007
    Age
    33
    Posts
    12,559
    Thanks given
    186
    Thanks received
    6,350
    Discord
    View profile
    Rep Power
    5000
    I've never really looked into the animation part of the client, since i've never really understood it. Thanks for this and thanks for taking the time to write this up.


    Reply With Quote  
     

  6. Thankful user:


  7. #5  
    Contributor

    rebecca's Avatar
    Join Date
    Aug 2017
    Posts
    1,000
    Thanks given
    797
    Thanks received
    848
    Discord
    View profile
    Rep Power
    5000
    awesome work suic, super valuable public info :3
    Reply With Quote  
     

  8. Thankful user:


  9. #6  






    Omar's Avatar
    Join Date
    Dec 2007
    Posts
    260
    Thanks given
    542
    Thanks received
    695
    Discord
    View profile
    Rep Power
    5000
    Great writeup Suic. Would rep if I could!


    Spoiler for aaaa:



    Reply With Quote  
     

  10. Thankful user:



Thread Information
Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)


User Tag List

Similar Threads

  1. RuneScape's Rendering and Animation System
    By The Wanderer in forum Informative Threads
    Replies: 5
    Last Post: 08-23-2011, 02:43 AM
  2. Swat's Animations System Fix
    By I'm me bitch in forum Snippets
    Replies: 7
    Last Post: 02-03-2010, 06:06 PM
  3. Swats Animation system with graphics?
    By Warlock 999 in forum Help
    Replies: 11
    Last Post: 10-21-2009, 01:34 AM
  4. Replies: 27
    Last Post: 08-19-2009, 10:22 PM
  5. RuneScape - New PVP System "Video"
    By sigex in forum Show-off
    Replies: 18
    Last Post: 01-17-2009, 03:44 AM
Posting Permissions
  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •