Difference between revisions of "Offset Compression"

From Second Life Wiki
Jump to navigation Jump to search
(Created page with "{{LSL Header}} = Offset Compression = I have often found in scripts that move objects or involve a lot of possible animations, that I have to handle (and store) a large number...")
 
Line 1: Line 1:
{{LSL Header}}
{{LSL Header}}
= Offset Compression =
= Offset Compression =
I have often found in scripts that move objects or involve a lot of possible animations, that I have to handle (and store) a large number of [[vector]] and [[rotation]] pairs describing offsets. Since each vector is three floats, and each rotation is four floats, and each float is 32-bits, that's 224 bits of data, plus overhead for lists. However, the rotation is normalised to ensure that its components never exceed a total of 1.0, meanwhile and offset vector rarely needs to be larger than 10 metres (the furthest a linked prim can be moved from its parent in most cases), as a result there is a lot of precision in that rotation that isn't really being used, since rotations don't generally need to be super-precise, and rarely so when used as part of an offset.
In scripts that involve object manipulation and/or offsets for animations it is common to end up storing a vector and rotation for every such interaction, which can be memory intensive if the object has a lot of values to store. However, such offsets typically don't need to be very precise as linked objects won't move far from their root prim, and small errors in the rotation shouldn't result in any major issues, so long as those rotations aren't used as a reference for future actions.


As a result I've designed the following functions to make it easy to store a vector plus a rotation in either a single vector or rotation value depending upon your requirements.
With this in mind, I've produced some functions that allow both a positional and rotational offset to be stored as a single vector value, effectively cutting memory requirements by more than half (especially when considering list overhead), though this comes at the cost of declaring the functions instead, so if only require these in a few places you may wish to consider inlining them instead.


== Offset as Rotation ==
== Functions ==
This method works by adding the vector values into the integer part of the corresponding rotation value by multiplying it by 1000 and rounding; this restricts the vector's precision to three decimal places, but is nice and simple. Rotations '''must''' be normalised before being passed to this function otherwise it will not decompress properly; any rotation retrieved using a standard LSL function should be okay, but user provided values should be [[Rotation#Normalizing_a_Rotation|normalised]] first.
For simplicity, these functions handle rotations as Euler vectors, which can be converted using [[llRot2Euler]]() and [[llEuler2Rot]](), this is also a convenient format for devices where rotation offsets may be specified in the user-friendly format in degrees (to match the build tools), such as in parsed notecards. These should still be converted to rotations for transformations in order to avoid [http://en.wikipedia.org/wiki/Gimbal_Lock Gimbal Lock]. '''NOTE:''' the combinedOffset() function below expects euler rotations to contain no component that exceeds positive or negative PI (just as the build tools do not allow this); for convenience the clampEulerRot() function is provided below to ensure that an Euler rotation is safe to use, but if your script is not vulnerable to this (for example, if your Euler rotations are created with llRot2Euler) then this function may be omitted.


In tests, there is an error of up to 0.5 degrees for the rotation; this should be precise enough for most use cases, but may become more noticeable with larger objects.
<source lang="lsl2">vector clampEulerRot(vector eulerRot) {
    @loopX1; @loopX2;
    if (eulerRot.x > PI) { eulerRot.x -= TWO_PI; jump loopX1; }
    else if (eulerRot.x < -PI) { eulerRot.x += TWO_PI; jump loopX2; }
    @loopY1; @loopY2;
    if (eulerRot.y > PI) { eulerRot.y -= TWO_PI; jump loopY1; }
    else if (eulerRot.y < -PI) { eulerRot.y += TWO_PI; jump loopY2; }
    @loopZ1; @loopZ2;
    if (eulerRot.z > PI) { eulerRot.z -= TWO_PI; jump loopZ1; }
    else if (eulerRot.z < -PI) { eulerRot.z += TWO_PI; jump loopZ2; }
    return eulerRot;
}


=== Functions ===
vector combinedOffset(vector offsetPos, vector offsetEulerRot) {
<source lang="lsl2">rotation offsetAsRot(vector offsetPos, rotation offsetRot) {
     return <
     return <
         (float)((integer)(offsetPos.x * 1000.0) * 10) + ((float)(((offsetPos.x < 0) * -2) + 1) * (offsetRot.x + 1.0)),  
         (float)((integer)(offsetPos.x * 1000.0) * 10) + ((float)(((offsetPos.x < 0) * -2) + 1) * (offsetEulerRot.x + 5.0)),  
         (float)((integer)(offsetPos.y * 1000.0) * 10) + ((float)(((offsetPos.y < 0) * -2) + 1) * (offsetRot.y + 1.0)),  
         (float)((integer)(offsetPos.y * 1000.0) * 10) + ((float)(((offsetPos.y < 0) * -2) + 1) * (offsetEulerRot.y + 5.0)),  
         (float)((integer)(offsetPos.z * 1000.0) * 10) + ((float)(((offsetPos.z < 0) * -2) + 1) * (offsetRot.z + 1.0)),
         (float)((integer)(offsetPos.z * 1000.0) * 10) + ((float)(((offsetPos.z < 0) * -2) + 1) * (offsetEulerRot.z + 5.0))
        offsetRot.s
     >;
     >;
}
}
vector offsetVectorFromRot(rotation offset) {
vector offsetToPos(vector offset) {
     return <
     return <
         (float)((integer)(offset.x / 10.0)) / 1000.0,  
         (float)((integer)(offset.x / 10.0)) / 1000.0,  
Line 26: Line 35:
     >;
     >;
}
}
rotation offsetRotFromRot(rotation offset) {
vector offsetToEulerRot(vector offset) {
     offset = <
     return <
         (offset.x - ((integer)(offset.x / 10.0) * 10)) * (float)(((offset.x < 0) * -2) + 1) - 1.0,
         (offset.x - ((integer)(offset.x / 10.0) * 10)) * (float)(((offset.x < 0) * -2) + 1) - 5.0,
         (offset.y - ((integer)(offset.y / 10.0) * 10)) * (float)(((offset.y < 0) * -2) + 1) - 1.0,
         (offset.y - ((integer)(offset.y / 10.0) * 10)) * (float)(((offset.y < 0) * -2) + 1) - 5.0,
         (offset.z - ((integer)(offset.z / 10.0) * 10)) * (float)(((offset.z < 0) * -2) + 1) - 1.0,
         (offset.z - ((integer)(offset.z / 10.0) * 10)) * (float)(((offset.z < 0) * -2) + 1) - 5.0
        offset.s
     >;
     >;
     float mag = llSqrt((offset.x * offset.x) + (offset.y * offset.y) + (offset.z * offset.z) + (offset.s * offset.s));
}</source>
     return <offset.x / mag, offset.y / mag, offset.z / mag, offset.s / mag>;
 
== Examples ==
=== Functions ===
The silly example below can be placed inside the root prim of a linked-set that you don't care much about (as it may be ruined). It will store the offsets of all prims and allow you to "crush" the object and then restore it by touching.
 
<source lang="lsl2">// Paste the above functions in here
 
list prim_offsets = []; integer crushed = FALSE;
default {
    state_entry() {
        integer prims = llGetNumberOfPrims();
        if (prims < 2) { llOwnerSay("This object is not a linked set"); return; }
       
        integer link;
        for (link = LINK_ROOT + 1; link <= prims; ++link) {
            list params = llGetLinkPrimitiveParams(link, [PRIM_POS_LOCAL, PRIM_ROT_LOCAL]);
            prim_offsets += [combinedOffset(llList2Vector(params, 0), llRot2Euler(llList2Rot(params, 1)))];
        }
        llSetText("Touch to crush!", <1.0, 1.0, 1.0>, 1.0);
     }
   
    changed(integer changes) { if (changes & CHANGED_LINK) llOwnerSay("Object has changed, script should be reset"); }
   
    touch_start(integer x) {
        // If the object was crushed, restore it
        if (crushed) {
            integer x = prim_offsets != [];
            while ((--x) >= 0) {
                vector offset = llList2Vector(prim_offsets, x);
                llSetLinkPrimitiveParamsFast(x + 2,
                    [PRIM_POS_LOCAL, offsetToPos(offset), PRIM_ROT_LOCAL, llEuler2Rot(offsetToEulerRot(offset))]
                );
            }
            llSetText("Touch to crush!", <1.0, 1.0, 1.0>, 1.0);
            crushed = FALSE;
        } else {
            integer x = prim_offsets != [];
            while ((--x) >= 0) llSetLinkPrimitiveParamsFast(x + 2, [PRIM_POS_LOCAL, ZERO_VECTOR, PRIM_ROT_LOCAL, ZERO_ROTATION]);
            llSetText("Ouch!", <1.0, 0.0, 0.0>, 1.0);
            crushed = TRUE;
        }
    }
}</source>
 
=== Inline ===
This second example is an identical script, but instead of using the functions as-is it shows how they can be inlined. You may wish to do this in a script that only uses offset conversion in a few places, e.g- on load and on use, as this will eliminate the memory and performance costs of declaring and executing functions.
 
<source lang="lsl2">list prim_offsets = []; integer crushed = FALSE;
default {
    state_entry() {
        integer prims = llGetNumberOfPrims();
        if (prims < 2) { llOwnerSay("This object is not a linked set"); return; }
       
        integer link;
        for (link = LINK_ROOT + 1; link <= prims; ++link) {
            list params = llGetLinkPrimitiveParams(link, [PRIM_POS_LOCAL, PRIM_ROT_LOCAL]);
            vector offsetPos = llList2Vector(params, 0);
            vector offsetEulerRot = llList2Vector(params, 1);
            prim_offsets += [< // Inline conversion to offset
                (float)((integer)(offsetPos.x * 1000.0) * 10) + ((float)(((offsetPos.x < 0) * -2) + 1) * (offsetEulerRot.x + 5.0)),
                (float)((integer)(offsetPos.y * 1000.0) * 10) + ((float)(((offsetPos.y < 0) * -2) + 1) * (offsetEulerRot.y + 5.0)),
                (float)((integer)(offsetPos.z * 1000.0) * 10) + ((float)(((offsetPos.z < 0) * -2) + 1) * (offsetEulerRot.z + 5.0))
            >];
        }
        llSetText("Touch to crush!", <1.0, 1.0, 1.0>, 1.0);
    }
   
    changed(integer changes) { if (changes & CHANGED_LINK) llOwnerSay("Object has changed, script should be reset"); }
   
     touch_start(integer x) {
        // If the object was crushed, restore it
        if (crushed) {
            integer x = prim_offsets != [];
            while ((--x) >= 0) {
                vector offset = llList2Vector(prim_offsets, x);
                llSetLinkPrimitiveParamsFast(x + 2,
                    [
                        PRIM_POS_LOCAL, < // Inline conversion from offset
                            (float)((integer)(offset.x / 10.0)) / 1000.0,  
                            (float)((integer)(offset.y / 10.0)) / 1000.0,  
                            (float)((integer)(offset.z / 10.0)) / 1000.0
                        >,
                        PRIM_ROT_LOCAL, llEuler2Rot(< // Inline conversion from offset
                            (offset.x - ((integer)(offset.x / 10.0) * 10)) * (float)(((offset.x < 0) * -2) + 1) - 5.0,
                            (offset.y - ((integer)(offset.y / 10.0) * 10)) * (float)(((offset.y < 0) * -2) + 1) - 5.0,
                            (offset.z - ((integer)(offset.z / 10.0) * 10)) * (float)(((offset.z < 0) * -2) + 1) - 5.0
                        >)]
                );
            }
            llSetText("Touch to crush!", <1.0, 1.0, 1.0>, 1.0);
            crushed = FALSE;
        } else {
            integer x = prim_offsets != [];
            while ((--x) >= 0) llSetLinkPrimitiveParamsFast(x + 2, [PRIM_POS_LOCAL, ZERO_VECTOR, PRIM_ROT_LOCAL, ZERO_ROTATION]);
            llSetText("Ouch!", <1.0, 0.0, 0.0>, 1.0);
            crushed = TRUE;
        }
    }
}</source>
}</source>

Revision as of 07:06, 16 December 2015

Offset Compression

In scripts that involve object manipulation and/or offsets for animations it is common to end up storing a vector and rotation for every such interaction, which can be memory intensive if the object has a lot of values to store. However, such offsets typically don't need to be very precise as linked objects won't move far from their root prim, and small errors in the rotation shouldn't result in any major issues, so long as those rotations aren't used as a reference for future actions.

With this in mind, I've produced some functions that allow both a positional and rotational offset to be stored as a single vector value, effectively cutting memory requirements by more than half (especially when considering list overhead), though this comes at the cost of declaring the functions instead, so if only require these in a few places you may wish to consider inlining them instead.

Functions

For simplicity, these functions handle rotations as Euler vectors, which can be converted using llRot2Euler() and llEuler2Rot(), this is also a convenient format for devices where rotation offsets may be specified in the user-friendly format in degrees (to match the build tools), such as in parsed notecards. These should still be converted to rotations for transformations in order to avoid Gimbal Lock. NOTE: the combinedOffset() function below expects euler rotations to contain no component that exceeds positive or negative PI (just as the build tools do not allow this); for convenience the clampEulerRot() function is provided below to ensure that an Euler rotation is safe to use, but if your script is not vulnerable to this (for example, if your Euler rotations are created with llRot2Euler) then this function may be omitted.

vector clampEulerRot(vector eulerRot) {
    @loopX1; @loopX2;
    if (eulerRot.x > PI) { eulerRot.x -= TWO_PI; jump loopX1; }
    else if (eulerRot.x < -PI) { eulerRot.x += TWO_PI; jump loopX2; }
    @loopY1; @loopY2;
    if (eulerRot.y > PI) { eulerRot.y -= TWO_PI; jump loopY1; }
    else if (eulerRot.y < -PI) { eulerRot.y += TWO_PI; jump loopY2; }
    @loopZ1; @loopZ2;
    if (eulerRot.z > PI) { eulerRot.z -= TWO_PI; jump loopZ1; }
    else if (eulerRot.z < -PI) { eulerRot.z += TWO_PI; jump loopZ2; }
    return eulerRot;
}

vector combinedOffset(vector offsetPos, vector offsetEulerRot) {
    return <
        (float)((integer)(offsetPos.x * 1000.0) * 10) + ((float)(((offsetPos.x < 0) * -2) + 1) * (offsetEulerRot.x + 5.0)), 
        (float)((integer)(offsetPos.y * 1000.0) * 10) + ((float)(((offsetPos.y < 0) * -2) + 1) * (offsetEulerRot.y + 5.0)), 
        (float)((integer)(offsetPos.z * 1000.0) * 10) + ((float)(((offsetPos.z < 0) * -2) + 1) * (offsetEulerRot.z + 5.0))
    >;
}
vector offsetToPos(vector offset) {
    return <
        (float)((integer)(offset.x / 10.0)) / 1000.0, 
        (float)((integer)(offset.y / 10.0)) / 1000.0, 
        (float)((integer)(offset.z / 10.0)) / 1000.0
    >;
}
vector offsetToEulerRot(vector offset) {
    return <
        (offset.x - ((integer)(offset.x / 10.0) * 10)) * (float)(((offset.x < 0) * -2) + 1) - 5.0,
        (offset.y - ((integer)(offset.y / 10.0) * 10)) * (float)(((offset.y < 0) * -2) + 1) - 5.0,
        (offset.z - ((integer)(offset.z / 10.0) * 10)) * (float)(((offset.z < 0) * -2) + 1) - 5.0
    >;
}

Examples

Functions

The silly example below can be placed inside the root prim of a linked-set that you don't care much about (as it may be ruined). It will store the offsets of all prims and allow you to "crush" the object and then restore it by touching.

// Paste the above functions in here

list prim_offsets = []; integer crushed = FALSE;
default {
    state_entry() {
        integer prims = llGetNumberOfPrims();
        if (prims < 2) { llOwnerSay("This object is not a linked set"); return; }
        
        integer link;
        for (link = LINK_ROOT + 1; link <= prims; ++link) {
            list params = llGetLinkPrimitiveParams(link, [PRIM_POS_LOCAL, PRIM_ROT_LOCAL]);
            prim_offsets += [combinedOffset(llList2Vector(params, 0), llRot2Euler(llList2Rot(params, 1)))];
        }
        llSetText("Touch to crush!", <1.0, 1.0, 1.0>, 1.0);
    }
    
    changed(integer changes) { if (changes & CHANGED_LINK) llOwnerSay("Object has changed, script should be reset"); }
    
    touch_start(integer x) {
        // If the object was crushed, restore it
        if (crushed) {
            integer x = prim_offsets != [];
            while ((--x) >= 0) {
                vector offset = llList2Vector(prim_offsets, x);
                llSetLinkPrimitiveParamsFast(x + 2, 
                    [PRIM_POS_LOCAL, offsetToPos(offset), PRIM_ROT_LOCAL, llEuler2Rot(offsetToEulerRot(offset))]
                );
            }
            llSetText("Touch to crush!", <1.0, 1.0, 1.0>, 1.0);
            crushed = FALSE;
        } else {
            integer x = prim_offsets != [];
            while ((--x) >= 0) llSetLinkPrimitiveParamsFast(x + 2, [PRIM_POS_LOCAL, ZERO_VECTOR, PRIM_ROT_LOCAL, ZERO_ROTATION]);
            llSetText("Ouch!", <1.0, 0.0, 0.0>, 1.0);
            crushed = TRUE;
        }
    }
}

Inline

This second example is an identical script, but instead of using the functions as-is it shows how they can be inlined. You may wish to do this in a script that only uses offset conversion in a few places, e.g- on load and on use, as this will eliminate the memory and performance costs of declaring and executing functions.

list prim_offsets = []; integer crushed = FALSE;
default {
    state_entry() {
        integer prims = llGetNumberOfPrims();
        if (prims < 2) { llOwnerSay("This object is not a linked set"); return; }
        
        integer link;
        for (link = LINK_ROOT + 1; link <= prims; ++link) {
            list params = llGetLinkPrimitiveParams(link, [PRIM_POS_LOCAL, PRIM_ROT_LOCAL]);
            vector offsetPos = llList2Vector(params, 0);
            vector offsetEulerRot = llList2Vector(params, 1);
            prim_offsets += [< // Inline conversion to offset
                (float)((integer)(offsetPos.x * 1000.0) * 10) + ((float)(((offsetPos.x < 0) * -2) + 1) * (offsetEulerRot.x + 5.0)), 
                (float)((integer)(offsetPos.y * 1000.0) * 10) + ((float)(((offsetPos.y < 0) * -2) + 1) * (offsetEulerRot.y + 5.0)), 
                (float)((integer)(offsetPos.z * 1000.0) * 10) + ((float)(((offsetPos.z < 0) * -2) + 1) * (offsetEulerRot.z + 5.0))
            >];
        }
        llSetText("Touch to crush!", <1.0, 1.0, 1.0>, 1.0);
    }
    
    changed(integer changes) { if (changes & CHANGED_LINK) llOwnerSay("Object has changed, script should be reset"); }
    
    touch_start(integer x) {
        // If the object was crushed, restore it
        if (crushed) {
            integer x = prim_offsets != [];
            while ((--x) >= 0) {
                vector offset = llList2Vector(prim_offsets, x);
                llSetLinkPrimitiveParamsFast(x + 2, 
                    [
                        PRIM_POS_LOCAL, < // Inline conversion from offset
                            (float)((integer)(offset.x / 10.0)) / 1000.0, 
                            (float)((integer)(offset.y / 10.0)) / 1000.0, 
                            (float)((integer)(offset.z / 10.0)) / 1000.0
                        >,
                        PRIM_ROT_LOCAL, llEuler2Rot(< // Inline conversion from offset
                            (offset.x - ((integer)(offset.x / 10.0) * 10)) * (float)(((offset.x < 0) * -2) + 1) - 5.0,
                            (offset.y - ((integer)(offset.y / 10.0) * 10)) * (float)(((offset.y < 0) * -2) + 1) - 5.0,
                            (offset.z - ((integer)(offset.z / 10.0) * 10)) * (float)(((offset.z < 0) * -2) + 1) - 5.0
                        >)]
                );
            }
            llSetText("Touch to crush!", <1.0, 1.0, 1.0>, 1.0);
            crushed = FALSE;
        } else {
            integer x = prim_offsets != [];
            while ((--x) >= 0) llSetLinkPrimitiveParamsFast(x + 2, [PRIM_POS_LOCAL, ZERO_VECTOR, PRIM_ROT_LOCAL, ZERO_ROTATION]);
            llSetText("Ouch!", <1.0, 0.0, 0.0>, 1.0);
            crushed = TRUE;
        }
    }
}