June 28, 2012

Rezzing prims with varying properties

If an object rezzes prims with varying properties (shape, size, color, texture, etc), it can rez a generic scripted prim and then send that prim a message containing a list of values to be given to llSetPrimitiveParams().

The prim to be rezzed would contain this script
 

integer rezzerChannel = -873648273;
integer settingsChannel = -873648274;
default
{
state_entry() { 

     llSetStatus( STATUS_DIE_AT_EDGE, TRUE );
     llListen( settingsChannel, "", NULL_KEY, "" );
}

on_rez( integer rezCode ) {
     if ( rezCode == 0 ) {
         llResetScript();
     } else {
         llSay(rezzerChannel , "primRezzed|"+(string)rezCode);
     }
}

listen ( integer channel, string fromName, key fromKey, string msg ) {
    list a = llParseStringKeepNulls( msg, ["|"], [] );
    if ( llList2String(a,0) == "primSettings" ) {
        if ( llList2String(a,-1) == "END" ) {
            integer n = (integer)llList2String(a,1);
            if ( n > 0 ) {
                list params = decodeTypeList( llList2List( a,2,n+1) );
                llSetPrimitiveParams( params );
            }
        } else {
            llSay(DEBUG_CHANNEL, "*** truncated message");
        }
        llRemoveInventory( llGetScriptName() );
    }
}
}

 
Notes on the rezzed prim script:

  • STATUS_DIE_AT_EDGE keeps errant prims out of your Lost and Found folder.
  • In on_rez(), the special handling of a rezCode of zero allows you to manually rez the prim. The rezzer should give a non-zero rezCode to each object it rezzes.
  • For non-zero rezCodes, the script sends a message back to the rezzer. The rezzer responds with the desired settings.
  • llSay() is sufficient because the prim must have been rezzed within 10 meters of the rezzer.
________________
A rezzer script that creates a green horizontal polygon could be written as follows:

integer rezzerChannel = -873648273;
integer settingsChannel = -873648274;

integer polyCount = 8;

default
{
state_entry() {
    llListen( rezzerChannel , "", NULL_KEY, "" );
}

touch_start( integer nTouch ) {
     
    string name = llGetInventoryName(INVENTORY_OBJECT,0);
    integer k; for ( k=0; k < polyCount; k++ ) {
        llRezObject( name, llGetPos(), ZERO_VECTOR,
                        ZERO_ROTATION, 100 + k);
    }
}

listen ( integer channel, string fromName, key fromKey, string msg ) {
    list a = llParseStringKeepNulls( msg, ["|"], [] );
    if ( llList2String(a,0) == "primRezzed" ) {
         integer index = (integer)llList2String(a,1);
       
        float height = 5;
        float radius = 15; 
        float width = 0.2;
        float length = 2 * radius * llTan( PI / polyCount );

        float angle = index * TWO_PI / polyCount;
        rotation cRot = llEuler2Rot( < PI_BY_TWO, 0,0> )
                * llEuler2Rot( <0, 0, angle > );

        vector cPos = llGetPos() + < 0,0 ,height> 
                         + < radius,0 ,0>* cRot;

        // [ PRIM_TYPE, PRIM_TYPE_CYLINDER, integer hole_shape,
        // vector cut, float hollow, vector twist,
        // vector top_size, vector top_shear ]
        list settingsList = [
            PRIM_TYPE, PRIM_TYPE_CYLINDER, 0, <0,1,0>, 0.0,
                <0,0,0>, <1,1,0>, <0,0,0>,
            PRIM_COLOR, ALL_SIDES, <0,1,0>, 1.0,
            PRIM_SIZE, < width, width, length >,
            PRIM_ROTATION, cRot, 
            PRIM_POSITION, cPos, PRIM_POSITION, cPos, 
            PRIM_POSITION, cPos ];
        settingsList = encodeTypeList( settingsList );

        llRegionSayTo( fromKey, settingsChannel, llDumpList2String(
            [ "primSettings", llGetListLength(settingsList) ]
            + settingsList
            + ["END"], "|" ));
    }
}
}
 
Notes on rezzer script:

  • It does not matter where llRezObject() places the prims since the subsequent message will specify the position.
  • Do not send prim settings in the object_rez() event. The prim may have rezzed, yet it may not have registered its listen event with the region.
  • SET_POSITION is limited to a 10 meter movement, so use several as needed. Given the height and radius used in this script, three was more than sufficient.
  • There is a limit to the number of messages that may be queued to the rezzer. If you will be rezzing more than 30 prims, use a timer to rez them in bursts of 20. Keep track of the number of rezzed prims and the number of messages received. Only rez bursts when the difference is less than 10.
 ________________

General notes:
  • The functions encodeTypeList() and decodeTypeList are described in the post "Interobject Communications: lists
  • If the call to llRemoveInventory() is removed from the rezzed prim script, then the rezzer could later change the settings of specific rezzed prims.  In this situation, the rezzed prim script should accept a "primScriptRemove" message.
 
  • Region managers frown on high performance rezzers. You might consider throttling the rezzing by putting a brief llSleep() after llRezObject(), which has an intrinsic delay of only 0.1 second.
  • As a clean-up courtesy to the region server, the rezzed prim script might call llSetTimerEvent()  when the rezCode is non-zero.  The timer event handler would call llDie().
[end]

June 19, 2012

Rotation: algebra

An earlier post on rotational pivoting used a correct yet misleading algebraic slight of hand.  Given the relationship

(1)    vecA = vecB * rotQ

the post solved for vecB by "dividing" both sides of the equation by rotQ:

(2)    vecB = vecA / rotQ

The LSL compiler accepts this expression, however, the concept of 'divide by a rotation' opens many doors to confused thinking.  When solving complex rotational problems, it is safer to think in terms of multiplication by inverse rotations.  

The inverse of rotQ is the rotation that undoes rotQ.   In LSL this inverse is designated using the expression "ZERO_ROTATION / rotQ".   By the meaning of 'inverse'

(3)    rotQ * ( ZERO_ROTATION / rotQ ) = ZERO_ROTATION
(4)    ( ZERO_ROTATION / rotQ ) * rotQ = ZERO_ROTATION   

_________
To solve for vecB using inverse rotations, right side multiplying each side of (1) by the inverse of rotQ

(5)    vecA * (ZERO_ROTATION / rotQ) = (vecB * rotQ) * (ZERO_ROTATION / rotQ)

Calculation precedence does not matter when multiplying vectors and rotation.  Thus the terms on the right side of (5) can be regrouped:

(6)    vecA * (ZERO_ROTATION / rotQ) = vecB * ( rotQ * (ZERO_ROTATION / rotQ) )

Using (3) on the right side of (6), (6) becomes
 
(7)    vecA * ( ZERO_ROTATION / rotQ ) = vecB * ZERO_ROTATION

Since ZERO_ROTATION means not rotating, and since not rotating vecB leaves it unchanged

(8)     vecB * ZERO_ROTATION = vecB.

and therefore

(9)    vecB = vecA * ( ZERO_ROTATION / rotQ )

___________________
If the proof of (3) and (4) seems to be a trivial rearrangement of terms, then you may be thinking in terms of numeric algebra, not vector and rotation algebra.  The use of inappropriate numeric algebraic rules is the cause of many types of logical errors when attempting to solve rotational algebra problems.

In (9) the expression "ZERO_ROTATION / rotQ" is a compiler syntactical trick.  It should not be mentally parsed into the items ZERO_ROTATION, /, and rotQ.  The compiler consumes it as a unit and emits the operation "compute inverse of rotQ".

In equation (2), the expression "vecA / rotQ" is also a compiler syntactic trick and should not be mentally parsed into items.  The LSL compiler treats "vecA / rotQ" as if it were "vecA * ( ZERO_ROTATION / rotQ )".  Thus (2) and (9) express the same result.

___________________
Notes:
  • The formal name for the rule (vecA * rotP) * rotQ = vecA * (rotP * rotQ) is associativity.  This rule means that the expression vecA * rotP * rotQ is unambiguous. 
  • The order of rotations matters.  vecA * rotP * rotQ != vecA * rotQ * rotP unless the rotations have the same axis.  This formal name for this property is non-commutative. 
  • Rotation is distributive when applied to the sum of two vectors.  ( vecA + vecB ) * rotQ = vecA * rotQ + vecB * rotQ

  • When multiplying vectors and rotations, the rotation are always on the right side.  This is a syntactical convention based on desiring vectors to be represented by a horizontal row of numbers in matrix algebra. 

  • The expression rotQ * vecA is meanless.
  • You cannot solve (1) for rotQ = vecA / vecB because a vector does not have an inverse.  This limitation reflects the existence of an infinite number of rotations that carry vecB to vecA.  However, there is a unique smallest angle rotation from vecB to vecA.  llRotBetween(vecB, vecA) gives you this smallest rotation.
  • You may fall into serious logical trouble if you write expressions like vecA / rotP / rotQ because rotational / is not associative, that is (vecA/rotP)/rotQ != vecA*(rotP/rotQ).

  • For more discussion of the rules of rotational algebra consult any text book on computer graphics mathematics and seek the chapters on spatial rotation.  If you consult non-CG text books you will discover the the rest of the world represents vectors as columns and uses left side rotational multiplication.
  • For a more formal discussion of non-commutative algebra read: Introduction to Lie Algebra by Erdmann and Wildon.

    • A common bit of advice is to remark that rotations are quaternions (they are not) and thus the inverse of rotQ can be created using rotQ.s = -rotQ.s.  LSL does indeed represent rotations by quaternions and this technique does work.  However, since the LSL compiler translates the abstract expression "ZERO_ROTATION / rotQ"  into the representation specific operations "tmp = rotQ; tmp.s = -tmp.s;", a laying-of-hands on the internals of a quaternion does not make a script more efficient. 

     [end]

    A deed-once land media manager

    Consider this script fragment:

    integer mediaChannel = -8782764;
    key authorizedUser = "817b2981-aeb6-42db-9478-1d525624c66e";

    default
    {
     
    state_entry() {
      llListen( mediaChannel, "", NULL_KEY, "" );
    }

    listen( integer channel, string fromName, key fromKey, string msg) {
      if ( (channel == mediaChannel)
                  && (llGetOwnerKey(fromKey)==(key)authorizedUser) ) {

        list a = llParseStringKeepNulls(msg,["|"], [] );
        string verb = llList2String(a,0);
        integer match = llListFindList(["setMedia", "queryMedia"],[verb]);
        if ( match >= 0 ) {
          string tag = llList2String(a,1);
          integer n = (integer)llList2String(a,2);
          if ( n > 0 ) {
            list b = decodeList( llList2List(a,3,2+n) );
            if ( match == 0 ) {
               llParcelMediaCommandList( b );
               llRegionSayTo(fromKey, mediaChannel, llDumpList2String(
                          [ verb+"Ack", tag, "ok" ], "|"));
            } else if ( match == 1 ) {
               list c = encodeList ( llParcelMediaQuery(b) );
               llRegionSayTo(fromKey, mediaChannel, llDumpList2String(
                          [ verb+"Ack", tag, "ok",
                              llGetListLength(c)] + c, "|"));
            }
          } else {
             llRegionSayTo(fromKey, mediaChannel, llDumpList2String(
                     [ verb+"Ack", tag, "fail", "no list" ], "|"));
          }
        }
      }
    }

    }

    If you add the encode/decode functions, place the script into a prim and then have that prim deeded to a parcel, then undeeded scripted objects that are owned by the authorizedUser and that send messages on the mediaChannel will be able to manage land media.

    If you are a land manager that hosts exhibits, then you can use this script to give your visiting exhibitors control of the land media without giving them the power to deed objects to the land.

    ____________________________
    Notes:
    • The encodeList() and decodeList() functions are described in the prior post "Interobject Communications: lists".
    • Sending media commands and query reults as encoded lists in messages means that you will never have to add features to this script.  If Linden Labs one day supports a new media action (PARCEL_MEDIA_COMMAND_whatever) then this script will already support that action.
    • The verb 'deed' it is shorthand for the longer, more awkward phrase "deed to a group that owns the parcel or have the parcel owner rez on the parcel".

    June 16, 2012

    Rotation: pivot around a child prim

    Given that an a object can pivot around any point, an object can pivot around a child prim by pivoting around the global position of the child prim.

    Given these values:

    vector gloVecObj    -- the global position of the root prim of an object
    rotation gloRotObj -- the global rotation of the root prim of an object

    vector locVecChild --  the local position of a child prim of an object

    use this fundamental relation between positions and rotations represented in different frame of reference (cf. "Rotation: pivot around a point"):

    (1)    gloVecA = gloVecObj + locVecA * gloRotObj 

    where A will be the child prim.  Thus 

     (2)    gloVecChild = gloVecObj + locVecChild * gloRotObj

    The calculation result of (2), gloVecChild, is the global vector of pivot point.

    ____________________________
    To script this in the root prim, note that

          vector gloVecObj = llGetPos();
          rotation gloRotObj = llGetRot();

    and that for the child with link number 'link'

          list a  = llGetLinkPrimitiveParams( link, [ PRIM_POS_LOCAL ] );
          vector locVecChild = llList2Vector( a, 0 );

    The post "Rotation: pivot around a point" describes how to use this value to perform the pivoting.

    [end]

    June 7, 2012

    Rotation: pivot around a point

    For this discussion there will be two frames of reference: global and local, local being the frame of reference attached to an object's root prim.  Any position can be represented as a vector in either the global or local frame.  In this specific problem, rotations are always represented in the global frame.

    The object has a starting global position and rotation and an ending global position and rotation.  There is a global pivot position.  The algebraic problem to solve is as follows:  Given the pivot position, the starting position, the starting rotation and the ending rotation, determine the ending position.

    The specific relationship for solving this specific problem is that "to pivot about" means that the local vector of the pivot point does not change.

    The general relationship for solving all rotation problems is that for any position, A, a rotation transforms a local vector into global vector in this manner:

    (1)    gloVecA = gloVecObj + locVecA * gloRotObj

    ____________________________
    Represent the beginning and ending positions and rotations of the object by:

             gloVecObjBeg and gloVecObjEnd
             gloRotObjBeg and gloRotObjEnd

    The global position of the pivot point does not change.  Represent it by:

             gloVecPivot

    By the meaning of  "to pivot about" the local position of the pivot point also does not change.  Represent it by:

             locVecPivot

    The local and global pivot positions are related by the general equation (1):

    (2)     gloVecPivot = gloVecObjBeg + locVecPivot * gloRotObjBeg 
    (3)     gloVecPivot = gloVecObjEnd + locVecPivot * gloRotObjEnd   


    Equations (2) and (3) will solved for the local pivot vector which will be elimiated by equating.  The resulting equation will be solved for gloVecPivotEnd in terms of the other known variables.

    ___________________________________
    Step A)        Solve (2) and (3) for the local vectors

    (4)    locVecPivot = ( gloVecPivot - gloVecObjBeg ) /gloRotObjectBeg
    (5)    locVecPivot = ( gloVecPivot - gloVecObjEnd ) / gloRotObjectEnd

    Step B)        Equate the right side expressions of Equations (4) and (5)

    (6)     ( gloVecPivot - gloVecObjBeg ) / gloRotObjBeg
                        =  ( gloVecPivot - gloVecObjEnd ) / gloRotObjEnd

    Step C)        Multiple both sides of (6) by gloRotObjectEnd

    (7)    ( ( gloVecPivot - gloVecObjBeg ) / gloRotObjBeg ) * gloRotObjEnd
                        =  gloVecPivot - gloVecObjEnd 

    Step D)        Rearrange (7) to isolate gloVecObjectEnd

    (8)     gloVecObjEnd = gloVecPivot
                    -  ( ( gloVecPivot - gloVecObjBeg ) / gloRotObjBeg ) * gloRotObjEnd

    __________________________________
    To script this, note that

          gloVecObjBeg = llGetPos();
          gloRotObjBeg = llGetRot();

    and that the script knows gloVecPivot and that the script has calculated a value for gloRotObjEnd.

    After using equation (8) to calculate gloVecObjEnd, the script will change the postion and rotation of the object:

         llSetPrimitiveParams( [ PRIM_POSITION, gloVecObjEnd,
                                                                       PRIM_ROTATION, gloRotObjEnd ] );

    ___________________________
    Notes:
    • The labeling of variables is complex in this problem because it is essential to keep track of the frame of reference of any vector or rotation value.
    • Do not invert the order of rotations in equation (8).
    • Use llSetPrimitiveParameters() to avoid seeing the object shift and rotate in two steps.

    [end]

    Interobject Communications: lists

    Lists contain values of varying types.  If llDumpListString() is used to create a text string which is to be sent as a message, then the value type information is irretrievably lost.

    Preserve the type information by creating a new list in which the type code precedes each value.  The sender might have code like this:

        list a = [ ... some series of values ];
        list b = encodeList(a);
        llSay( commChannel, llDumpList2String( [ "listData", llGetListLength(b) ]
                                   encodeList(b) + ["END"] , "|" );

    The receiver might have code like this

    listen( integer channel, string fromName, key fromKey, string msg ) {
        if ( channel == commChannel ) {
            list f = llParseStringKeepNulls( msg, "|" );
            if ( "listData" == llList2String(f,0) ) {
                if ( "END" != llList2String(f,-1) ) {
                    ... tell some that the message has been truncated
                } else {
                    integer n = (integer) llList2String(f,1);
                    list dataList = [];
                    if ( n > 0 ) {
                       dataList = decode( llList2List( f, 2, 1 + n );
                    }
                    n = llGetListLength(dataList);
                    ........
                }
            }
         }
    }

    The encoder logic might be this

    list typeList = [TYPE_FLOAT, TYPE_INTEGER, TYPE_KEY,
                       TYPE_ROTATION, TYPE_STRING, TYPE_VECTOR ];

    list encodeTypeList( list a ) {
        list b = [];
        integer n = llGetListLength(a);
        integer k;  for ( k=0; k < n; k++ ) {
            integer type = llGetListEntryType(a,k);
            integer match = llListFindList( typeList, [type] );
            if ( match >= 0 ) {
                b += [type] + llList2List(a,k,k);
            } else llOwnerSay("** failed to match list entry type: "+(string)type);
        }
        return b;
    }

    The decoder logic might be this:

    list decodeTypeList( list a ) {
        list b = [];
        integer n = llGetListLength(a);
        integer k;  for ( k=0; k < n; k += 2 ) {
            integer type = (integer) llList2String(a,k);
            string svalue = llList2String(a,k+1);
          
            integer match = llListFindList( typeList, [type] );
            if ( match >= 0 ) {
                if ( match-- == 0 )     b += (float) svalue;
                if ( match-- == 0 )     b += (integer) svalue;
                if ( match-- == 0 )     b += (key) svalue;
                if ( match-- == 0 )     b += (rotation) svalue;
                if ( match-- == 0 )     b += svalue;
                if ( match-- == 0 )     b += (vector) svalue;
            }
        }
        return b;
    }

    __________________

    Notes:
    • When sending variable length lists, append "END" to the end of the message and have the receiver verify that it is still there.  There is a limit to the length of messages.
    • Include the encoded list length in the message, so that the receiver can extract the list.
    • Since lists may have zero length, the receiver should handle the zero length case.
    [end]