Jump to content

Rotations.


Quistess Alpha
 Share

Recommended Posts

I started this about a month ago as a sort of guide/How to on rotations, but it turns out I'm not amazing at explaining things, so it kind of morphed into a library of functions with some filler in-between. Had I all the time in the world maybe I would expand my verbiage, but I've gotten tired of thinking about it, so hopefully it's still useful, at the very least for the code snippets.

Part 0: Non-rotational geometry problems.
    0a: Gimme a point!
        For getting the Position of an in-world object of interest, use
            http://wiki.secondlife.com/wiki/LlGetPos
            http://wiki.secondlife.com/wiki/LlGetLinkPrimitiveParams
            http://wiki.secondlife.com/wiki/LlGetObjectDetails   or occasionally
            http://wiki.secondlife.com/wiki/LlDetectedPos
        Since any two points define a line, you can take a weighted sum of two (or more) points to get another point on the line they define:

vector PosInterp(vector a,vector b,float f)
{   return a*(1-f) + b*(f);
}


        Will return a vector f perunit(percent/100) of the way between a and b.
        it's sometimes easier to think about it as scaling a vector:

return a+f*(b-a);

        is mathematically equivalent.

    0b: Gimme a length!
        Woa there's a function that does exactly this:
            http://wiki.secondlife.com/wiki/LlVecDist
        As noted in the wiki, you can use llVecMag(a-b); instead.
    0c: Gimme an angle!
        <insert discussion on the differences between radians and degrees>
        Of all the trigonometric functions, llAtan2() is the answer to 'how do I find this angle?' in 90% of cases you will run into in LSL:
            http://wiki.secondlife.com/wiki/LlAtan2
        The wikipedia article linked to from the wiki explains it fairly well.
        It can be quite useful to pair this with:
            http://wiki.secondlife.com/wiki/LlDetectedTouchST
        to make (for example) a turnable dial:
            rez a cylinder prim and set its dimensions to 0.5,0.5,0.05, and link a Prism with dimensions 0.25, 0.05, 0.05 on top of it.
            link the Prism to the Cylinder (cylinder as the root) and add this script:

default
{   touch(integer total_number)
    {   vector touchST = llDetectedTouchST(0)-<0.5,0.5,0>;
        // subtract 0.5 from both coordinates to move the origin from the corner to the center of the face.
        llSetLinkPrimitiveParamsFast(2,
        [   PRIM_ROT_LOCAL, llAxisAngle2Rot(<0,0,1>,
            //There is never a good reason to use PRIM_ROTATION, which only works on the root prim.
            llAtan2(touchST.y,touchST.x))
            //It can be confusing to remember that y comes before x.
        ]);
    }
}


        For calculating the angle between three arbitrary points in (3d) space you can use:

float uAngleBetween(vector a,vector b,vector c)
{   return llRot2Angle(llRotBetween(c-b,a-b));
}


        Note that this will only return a positive angle between 0 and PI radians (0 and 180 degrees);
        In 3d space, there is no way to distinguish between angles abc and cba like in 2d space. (imagine flipping over the piece of paper the angle is on and looking at it from the other side)
       

Part 1: Rotations as specific Orientations.
    There are (at least) 2 semantically different ways of thinking about vectors in 3-dimensional space:
    1. As a transformation. You can visualize a vector as a little arrow that points from the origin to some point in space, and by applying that arrow to the origin, you get the point in question.
    2. As a static 'point'. The result of applying a vector to the origin is the point represented by the vector.
    In a similar way, you can either think of a rotation as a transformation which rotates a reference frame in some way, or as the result of that transformation. While most other tutorials and introductions to rotations lean on the first method of understanding, as it is the most useful for understanding the calculations behind rotations, Understanding them as static orientations, is in my view often easier for using them in a practical context.

    1a: getting and setting orientations.
        Note that there is a small bug with llSLPPF:
            https://jira.secondlife.com/browse/SVC-93
        when setting the rotation of a child prim, it is important to use PRIM_ROT_LOCAL rather than PRIM_ROTATION.
        To set a prim to a rotation obtained from the build menu, use:
        llSLPPF(link,[PRIM_ROT_LOCAL,llEuler2Rot(<x,y,z>*DEG_TO_RAD)]);
        When setting a prims rotation like this, it is always relative to the rotation of the root. you can cancel out the root's rotation to set to a global orientation:
        llSLPPF(link,[PRIM_ROT_LOCAL,llEuler2Rot(<x,y,z>*DEG_TO_RAD)/llGetRootRotation()]);
        The 24 rotational symmetries of a cube can occasionally come in handy:
 

list Rotations =
[ // Notice: in LSL the real component of the quaternion is listed last. In other words: i,j,k,r.
  // I don't think there's much logic to whether I chose the positive or negative version of any given rotation.
    < 0.00, 0.00, 0.00, 1.00>,"Face East; Up Up   ", //
    < 0.00, 0.00, 0.71, 0.71>,"Face North; Up Up  ", //
    < 0.00, 0.00, 1.00, 0.00>,"Face West; Up Up   ", //
    < 0.00, 0.00, 0.71,-0.71>,"Face South; Up Up  ",//
                                                       
    < 1.00, 0.00, 0.00, 0.00>,"Face East; Up Down ", //
    < 0.71, 0.71, 0.00, 0.00>,"Face North; Up Down",//
    < 0.00, 1.00, 0.00, 0.00>,"Face West; Up Down ", //
    < 0.71,-0.71, 0.00, 0.00>,"Face South; Up Down",//
                                                       
    < 0.00, 0.71, 0.00, 0.71>,"Face Down; Up East ", //
    < 0.50,-0.50, 0.50,-0.50>,"Face South; Up East",//
    < 0.71, 0.00, 0.71, 0.00>,"Face Up; Up East   ", //
    < 0.50, 0.50, 0.50, 0.50>,"Face North; Up East",//
                                                       
    <-0.50, 0.50, 0.50, 0.50>,"Face Down; Up North",//    
    < 0.00, 0.71, 0.71, 0.00>,"Face West; Up North", //
    < 0.50, 0.50, 0.50,-0.50>,"Face Up; Up North  ",//
    < 0.71, 0.00, 0.00,-0.71>,"Face East; Up North",//
                                                       
    < 0.71, 0.00,-0.71, 0.00>,"Face Down; Up West ",//
    <-0.50, 0.50, 0.50,-0.50>,"Face South; Up West",//
    < 0.00, 0.71, 0.00,-0.71>,"Face Up; Up West   ",//
    < 0.50, 0.50,-0.50,-0.50>,"Face North; Up West",//
                                                       
    < 0.50, 0.50,-0.50, 0.50>,"Face Down; Up South",//
    < 0.00, 0.71,-0.71, 0.00>,"Face West; Up South",//
    < 0.50,-0.50, 0.50, 0.50>,"Face Up; Up South  ",//  
    < 0.71, 0.00, 0.00, 0.71>,"Face East; Up South", //

// - - - - - - - -

    < 0.00, 0.00, 0.00, 1.00>,"1", //face east up up
    < 0.00, 0.00, 1.00, 0.00>,"2", //face west up up
    < 0.00, 1.00, 0.00, 0.00>,"3", //face west up down
    < 1.00, 0.00, 0.00, 0.00>,"4", //face east up down
    < 0.00, 0.00, 0.71, 0.71>,"5", //face north up up
    < 0.00, 0.71, 0.00, 0.71>,"6", //face down up east
    < 0.71, 0.00, 0.00, 0.71>,"7", //face east up south
    < 0.00, 0.71, 0.71, 0.00>,"8", //face west up north
    < 0.71, 0.00, 0.71, 0.00>,"9", //face up up east
    < 0.71, 0.71, 0.00, 0.00>,"10",//face north up down
    < 0.00, 0.00, 0.71,-0.71>,"11",//face south up up
    < 0.00, 0.71, 0.00,-0.71>,"12",//face up up west
    < 0.71, 0.00, 0.00,-0.71>,"13",//face east up north
    < 0.00, 0.71,-0.71, 0.00>,"14",//face west up south
    < 0.71, 0.00,-0.71, 0.00>,"15",//face down up west
    < 0.71,-0.71, 0.00, 0.00>,"16",//face south up down
    < 0.50, 0.50, 0.50, 0.50>,"17",//face north up east
    < 0.50, 0.50, 0.50,-0.50>,"18",//face up up north
    < 0.50, 0.50,-0.50, 0.50>,"19",//face down up south
    < 0.50,-0.50, 0.50, 0.50>,"20",//face up up south
    <-0.50, 0.50, 0.50, 0.50>,"21",//face down up north
    < 0.50, 0.50,-0.50,-0.50>,"22",//face north up west
    < 0.50,-0.50, 0.50,-0.50>,"23",//face south up east
    <-0.50, 0.50, 0.50,-0.50>,"24",//face south up west

    ZERO_ROTATION,"ZERO_ROTATION"// no final Comma.

];
integer index=-1;
default
{
    touch_start(integer total_number)
    {   ++index;
        if(index==llGetListLength(Rotations))
        {   index = 0;
        }
        rotation rot = llList2Rot(Rotations,index)
        llSetRot(rot);
        ++index;
        llSetText(
            llList2String(Rotations,index),
            <0.0,1.0,0.0>,1.0);
    }
}

1b: Yaw Pitch Roll
    Unfortunately llEuler2Rot and llRot2Euler do not use a convention which is immediately intuitive. The equivalent functions for Yaw,Pitch and Roll
        https://simple.wikipedia.org/wiki/Pitch,_yaw,_and_roll

rotation YPR2Rot(vector v) // Yaw Pitch Roll
{   return
        <llSin(v.z/2),0,0,llCos(v.z/2)> *
        <0,llSin(v.y/2),0,llCos(v.y/2)> *
        <0,0,llSin(v.x/2),llCos(v.x/2)> ;
}
vector Rot2YPR(rotation q)
{ //implementation by Mollymews
// https://community.secondlife.com/forums/topic/471561-urot2euler/?do=findComment&comment=2293046
    vector v;
    // x-axis
    float s = 2 * (q.s * q.x + q.y * q.z);
    float c = 1 - 2 * (q.x * q.x + q.y * q.y);
    v.z = llAtan2(s, c);
    // y-axis
    s = 2 * (q.s * q.y - q.z * q.x);
    if (llFabs(s) >= 1)
        v.y = (-1 | (s < 0)) * PI_BY_TWO;
    else
        v.y = llAsin(s);
    // z-axis
    s = 2 * (q.s * q.z + q.x * q.y);
    c = 1 - 2 * (q.y * q.y + q.z * q.z);
    v.x = llAtan2(s, c);
    return v;
}
vector Rot2YPR(rotation rot)
{//my alternate implementation
    vector ret;
    vector temp = <1,0,0>*rot;
    ret.x = llAtan2(temp.y,temp.x);
    rot = rot/llEuler2Rot(<0,0,ret.z>);
    temp = <1,0,0>*rot;
    ret.y = -llAtan2(temp.z,temp.x);
    rot = rot/llEuler2Rot(<0,ret.y,0>);
    temp = <0,1,0>*rot;
    ret.z = llAtan2(temp.z,temp.y);
    return ret;
}

1c LookAt
    While ll provides 2 functions llLookAt() and llRotLookAt() to make an object look at a target, they are a bit limited in scope, and not as generally useful as being able to calculate the rotation which would cause your object to look at a given target.
    This is easily doable by combining llAtan2 with YPR2rot:
    

rotation uVectorRoll2Rot(vector v,float r)
{   return YPR2Rot(<llAtan2(v.y,v.x),-llAtan2(v.z,llVecMag(<v.x,v.y,0>)),r>);
}

   
    This will return a rotation that points the local x-axis (the forward vector) to vector v.
    Note that there is a similar ll function:
        http://wiki.secondlife.com/wiki/LlRotBetween
    but due to how rotations work, this can introduce some extra roll into the rotation if used carelessly.
    try it in a basic look-at-owner script.
 

default
{   state_entry()
    {   while(TRUE)
        {   vector target=llList2Vector(
                llGetObjectDetails(llGetOwner(),
                [OBJECT_POS]),0)-llGetPos();
            //Use one or the other and compare:
            rotation rot = // choose 1
                uVectorRoll2Rot(target,0);
                //llRotBetween(<1,0,0>,target);
                //llAxisAngle2Rot(<0,0,1>,llAtan2(target.y,target.x));
            //we can change the 'forward axis' with a left multiplication:
            //rot = llRotBetween(<0,0,1>/*forward axis*/ , <1,0,0>) * rot;
            llSetRot(rot);
        }
    }
}

1d: Reference Frames
    While in an orientation-centric mindset, we can understand the product of a vector v and a rotation r (v*r) as 'place v into reference frame r' and similarly we can view the product of 2 rotations q and r as 'place q in reference frame r':

uMoveToTargetKey(key target, vector offset, rotation localRot)
{  list det = llGetObjectDetails(target,[OBJECT_POS,OBJECT_ROT]);
    vector targetPos=llList2Vector(det,0);
    rotation targetRot = llList2Rot(det,1);
    llSetLinkPrimitivParamsFast(LINK_THIS,
    [   PRIM_POSITION,targetPos+offset*targetRot,
        PRIM_ROTATION, localRot*targetRot
    ]);
}

1e: interpolation
    Similarly to

vector PosOnLine(vector a,vector b,float f)
{   return a*(1-f) + b*(f);
}

    you can linearly interpolate between quaternions:

rotation RotInterp(rotation a,rotation b,float f)
{   return // return a rotation f perunit of the way between a and b.
        <a.x*(1-f) + b.x*f ,
         a.y*(1-f) + b.y*f ,
         a.z*(1-f) + b.z*f ,
         a.s*(1-f) + b.s*f>;
}// or at least it seems to work.

    but this will give you a non-normalized quaternion (see below).
    A more correct way of doing this would be:

rotation RotInterp(rotation a,rotation b,float f)
{//alternate implementation.
    rotation rot = llRotBetween(llRot2Fwd(a),llRot2Fwd(b));
    //Untested but should be equivalent:
    //rotation rot = (ZERO_ROTATION/a)*b;
    return a*llAxisAngle2Rot(llRot2Axis(rot),llRot2Angle(rot)*f);
}

2: miscellaneous.
    
    llEuler2Rot(<x,0,0>); == llAxisAngle2Rot(<1,0,0>,x); == <llSin(x*0.5),0,0,llCos(x*0.5)>;
    llEuler2Rot(<0,y,0>); == llAxisAngle2Rot(<0,1,0>,y); == <0,llSin(y*0.5),0,llCos(y*0.5)>;
    llEuler2Rot(<0,0,z>); == llAxisAngle2Rot(<0,0,1>,z); == <0,0,llSin(z*0.5),llCos(z*0.5)>;
    rotation uAxisAngle2Rot(vector v, float a)
    {   v*= llSin(a*0.5);
        return <v.x,v.y,v.z,llCos(a*0.5)>;
    }
    
    Consider using llAxisAngle2Rot() instead of llEuler2Rot() in example scripts.
    llEuler2Rot is really only useful for single axis-rotations and using the numbers obtained from the build window.

    <1,0,0>*<0,0,0,3> == <9,0,0>;
    but llSetRot(<0,0,0,3>); does not scale an object by 9.
    Quaternion multiplication scales a vector by the square of the quat's magnitude.
    use

rotation NormRot(rotation Q)
{   // taken from the wiki http://wiki.secondlife.com/wiki/LlSetKeyframedMotion
    float MagQ = llSqrt(Q.x*Q.x + Q.y*Q.y +Q.z*Q.z + Q.s*Q.s);
    return
        <Q.x/MagQ, Q.y/MagQ, Q.z/MagQ, Q.s/MagQ>;
}

    to normalize a quaternion so that it does not scale a vector.
    
    
    <-x,-y,-z,-r> represents the same rotation as <x,y,z,r>
    <-x,-y,-z,+r> == <0,0,0,1>/<x,y,z,r> == the 'opposite' or inverse of <x,y,z,r>

Edited by Quistessa
sudden epiphany for RotInterp.
  • Like 3
  • Thanks 3
Link to comment
Share on other sites

 -- extra scripts - - -
Gimbals 1: this is a touch to set gimbal.
also, this is 4 scripts in 1, uncomment/recomment the relevant lines to switch between:
//XYZ intrinsic (local),
//ZYX intrinsic (local), (Pitch, Yaw, Roll)
//XYZ extrinsic (global),
//ZYX extrinsic (global),

list gimbal_links=[0,0,0];

vector Euler_rot=<0,0,0>;
unrot_gimbals()
{   rotation RR = llGetRootRotation();
    //we cancel out the root's rotation along the axis of the gimbal so that the uv (used in llAtan2) still makes sense.
    llSetLinkPrimitiveParamsFast(
        llList2Integer(gimbal_links,0),
        [PRIM_ROT_LOCAL,<0.00,0.71,0.00,0.71>
        *llEuler2Rot(<-Euler_rot.x,0,0>)]); // local gimbal
        // /RR]); // global gimbal
    llSetLinkPrimitiveParamsFast(
        llList2Integer(gimbal_links,1),
        [PRIM_ROT_LOCAL,<0.71,0.00,0.00,0.71>
        *llEuler2Rot(<0,-Euler_rot.y,0>)]); // local gimbal
        // /RR]); // global gimbal
    llSetLinkPrimitiveParamsFast(
        llList2Integer(gimbal_links,2),
        [PRIM_ROT_LOCAL,
        llEuler2Rot(<0,0,-Euler_rot.z>)]); // local gimbal
        // /RR]); // global gimbal
}

rotation uEuler2Rot2(vector v)
{   return //ZYX intrinsic = XYZ extrinsic
        <llSin(v.x/2),0,0,llCos(v.x/2)> *
        <0,llSin(v.y/2),0,llCos(v.y/2)> *
        <0,0,llSin(v.z/2),llCos(v.z/2)> ;
}

default
{
    state_entry()
    {
        //find gimbal link_numbers
        integer index = llGetNumberOfPrims()+1;
        while(--index>=2)
        {   if(llGetLinkName(index)=="Gimbal X")
            {   gimbal_links=llListReplaceList(
                    gimbal_links,[index],0,0);
                llSetLinkPrimitiveParamsFast(index,
                [   PRIM_TEXTURE, 0, "b2d5eb75-b4bf-7763-28dc-d46a03f8e992",
                    <1,1,0>,<0,0,0>,-PI_BY_TWO,
                    PRIM_TEXTURE, 3, "571d44bb-8e5f-a579-eaec-cdb0bd109b29",
                    <1,1,0>,<0,0,0>,-PI_BY_TWO
                ]);
            }else if(llGetLinkName(index)=="Gimbal Y")
            {   gimbal_links=llListReplaceList(
                    gimbal_links,[index],1,1);
                llSetLinkPrimitiveParamsFast(index,
                [   PRIM_TEXTURE, 0, "571d44bb-8e5f-a579-eaec-cdb0bd109b29",
                    <1,1,0>,<0,0,0>,-PI_BY_TWO,
                    PRIM_TEXTURE, 3, "b2d5eb75-b4bf-7763-28dc-d46a03f8e992",
                    <1,1,0>,<0,0,0>,-PI_BY_TWO
                ]);
            }else if(llGetLinkName(index)=="Gimbal Z")
            {   gimbal_links=llListReplaceList(
                    gimbal_links,[index],2,2);
                llSetLinkPrimitiveParamsFast(index,
                [   PRIM_TEXTURE, 0, "b2d5eb75-b4bf-7763-28dc-d46a03f8e992",
                    <1,1,0>,<0,0,0>,-PI_BY_TWO,
                    PRIM_TEXTURE, 3, "571d44bb-8e5f-a579-eaec-cdb0bd109b29",
                    <1,1,0>,<0,0,0>,-PI_BY_TWO
                ]);
            }
        }
        //initialize rotation and settext.
        llSetText("ZYX intrinsic\n"+(string)Euler_rot,<0.0,1.0,0.0>,1.0);
        llSetRot(llEuler2Rot(Euler_rot));
        unrot_gimbals();
    }

    touch_start(integer total_number)
    {
        integer link = llDetectedLinkNumber(0);
        vector st = llDetectedTouchST(0);
        // -0.5 puts the origin in the center
        float angle = llAtan2(st.y-0.5,st.x-0.5);
        //llOwnerSay((string)llDetectedTouchFace(0));
        integer face = llDetectedTouchFace(0);
        if(link==llList2Integer(gimbal_links,0)) // x gimbal
        {   if(face==0)
            {   Euler_rot.x=angle;
            }else if(face==3)
            {   Euler_rot.x=-angle;
            }
        }else if(link==llList2Integer(gimbal_links,1)) // y gimbal
        {   if(face==3)
            {   Euler_rot.y=angle;
            }else if(face==0)
            {   Euler_rot.y=-angle;
            }
        }else if(link==llList2Integer(gimbal_links,2)) // y gimbal
        {   if(face==0)
            {   Euler_rot.z=angle;
            }else if(face==3)
            {   Euler_rot.z=-angle;
            }
        }else
        {   Euler_rot=<0,0,0>;
        }
        llSetText("ZYX intrinsic\n"+(string) //use uEuler2Rot2
            (Euler_rot*RAD_TO_DEG),<0.0,1.0,0.0>,1.0);
        //llSetText("ZYX extrinsic\n"+(string) //use llEuler2Rot
        //    (Euler_rot*RAD_TO_DEG),<0.0,1.0,0.0>,1.0);
        //llSetText("XYZ extrinsic\n"+(string) //use uEuler2Rot2
        //    (Euler_rot*RAD_TO_DEG),<0.0,1.0,0.0>,1.0);
        //llSetText("XYZ intrinsic\n"+(string) //use llEuler2Rot
        //    (Euler_rot*RAD_TO_DEG),<0.0,1.0,0.0>,1.0);
        llSetRot(uEuler2Rot2(Euler_rot));
        //llSetRot(llEuler2Rot(Euler_rot));
        unrot_gimbals();
    }
}


***
Gimbals 2
these are click-drag to rotate.
2 scripts in one, see comments.
Should auto-assemble after touched if linked prims are named correctly,
suggested gimbal radii 0.8 1.0 and 1.25 with 80% hollow, other objects all dimensions 0.2.
***

list gimbal_links=[0,0,0];
list object_links =[0,0,0,0,0,0,0];
//Base,UP,DWN,LFT,RGH,FWD,BCK
list object_offsets = // when root in zero_rotation.
[   <0.00,1.00,0.00>, // first is offset from root, then offsets from this offset.
    < 0.00, 0.00, 0.20>, // UP
    < 0.00, 0.00,-0.20>, //DWN
    < 0.00, 0.20, 0.00>, //LFT
    < 0.00,-0.20, 0.00>, //RGH
    < 0.20, 0.00, 0.00>, //FWD
    <-0.20, 0.00, 0.00> //BCK

];
list gimbal_offsets =
[
    < 0.00, 1.00, 0.00>, //Gimbal X
    < 0.00, 0.00, 0.00>, //Gimbal Y
    < 0.00, 0.00, 0.00>  //Gimbal Z
];
list gimbal_rotations =
[
    < 0.00, 0.71, 0.00, 0.71 >, //Gimbal X
    < 0.71, 0.00, 0.00, 0.71 >, //Gimbal Y
    < 0.00, 0.00, 0.00, 1.00 > //Gimbal Z
];
list object_rotations = // when root in zero_rotation
[   < 0.00, 0.00, 0.00, 1.00 >, //Base
    < 0.00, 0.00, 0.00, 1.00 >, //UP
    < 1.00, 0.00, 0.00, 0.00 >, //DWN
    < 0.00, 0.71, 0.71, 0.00 >, //LFT
    < 0.71, 0.00, 0.00, 0.71 >, //RGH
    < 0.50, 0.50, 0.50, 0.50 >, //FWD
    <-0.50, 0.50, 0.50,-0.50 > //BCK
];
rotation gRotBase = <0,0,0,1>; // rotation of example structure
vector start_angle = <0,0,0>; //angle on touch_start.

uSetSubRot(list links,list offsets,list rotations,rotation rot)
{
    integer index = llGetListLength(links);
    if(index!=llGetListLength(offsets))
    {   llSay(DEBUG_CHANNEL,"list length mismatch in uSetSubRot()");
        return;
    }if(index!=llGetListLength(rotations))
    {   llSay(DEBUG_CHANNEL,"list length mismatch in uSetSubRot()");
        return;
    }
    vector base = llList2Vector(offsets,0);
    rotation baserot = llList2Rot(rotations,0);
    while(--index>=1)
    {
        llSetLinkPrimitiveParamsFast(llList2Integer(links,index),
            [PRIM_POS_LOCAL,
                base+
                llList2Vector(offsets,index)*
                rot*baserot,
            PRIM_ROT_LOCAL,
                llList2Rot(rotations,index)*
                rot
            ]);
    }   // set base rotation :
    llSetLinkPrimitiveParamsFast(llList2Integer(links,index),
        [PRIM_POS_LOCAL,
            llList2Vector(offsets,index),
        PRIM_ROT_LOCAL,
            llList2Rot(rotations,index)*
            rot
        ]);
}

vector uGetGimbalAngle() // must be run in a touch event.
{
    integer link = llDetectedLinkNumber(0);
    vector st = llDetectedTouchST(0);
    // -0.5 puts the origin in the center
    float angle = llAtan2(st.y-0.5,st.x-0.5);
    //llOwnerSay((string)llDetectedTouchFace(0));
    integer face = llDetectedTouchFace(0);
    vector return_angle;
    if(link==llList2Integer(gimbal_links,0)) // x gimbal
    {   if(face==0)
        {   return_angle.x=angle;
        }else if(face==3)
        {   return_angle.x=-angle;
        }
    }else if(link==llList2Integer(gimbal_links,1)) // y gimbal
    {   if(face==3)
        {   return_angle.y=angle;
        }else if(face==0)
        {   return_angle.y=-angle;
        }
    }else if(link==llList2Integer(gimbal_links,2)) // y gimbal
    {   if(face==0)
        {   return_angle.z=angle;
        }else if(face==3)
        {   return_angle.z=-angle;
        }
    }else
    {   return_angle=<0,0,0>;
    }
    return return_angle;
}

vector uRot2Euler2(rotation rot)
{
    vector ret;
    vector temp = <1,0,0>*rot;
    ret.z = llAtan2(temp.y,temp.x);
    rot = rot/llEuler2Rot(<0,0,ret.z>);
    temp = <1,0,0>*rot;
    ret.y = -llAtan2(temp.z,temp.x);
    rot = rot/llEuler2Rot(<0,ret.y,0>);
    temp = <0,1,0>*rot;
    ret.x = llAtan2(temp.z,temp.y);
    return ret;
}
vector uRot2Euler(rotation rot)
{
    vector ret;
    vector temp = <0,1,0>*rot;
    ret.x = llAtan2(temp.z,temp.y);
    rot = rot/llEuler2Rot(<ret.x,0,0>);
    temp = <1,0,0>*rot;
    ret.y = -llAtan2(temp.z,temp.x);
    rot = rot/llEuler2Rot(<0,ret.y,0>);
    temp = <1,0,0>*rot;
    ret.z = llAtan2(temp.y,temp.x);
    return ret;
}
default
{
    state_entry()
    {
        //find gimbal link_numbers
        integer index = llGetNumberOfPrims()+1;
        
        while(--index>=1)
        {   // I really should have set a variable to llGetLinkName(index) and checked against that,
            // but I'm too lazy to search-replace it.
            if(llGetLinkName(index)=="Gimbal X")
            {   gimbal_links=llListReplaceList(
                    gimbal_links,[index],0,0);
            }else if(llGetLinkName(index)=="Gimbal Y")
            {   gimbal_links=llListReplaceList(
                    gimbal_links,[index],1,1);
            }else if(llGetLinkName(index)=="Gimbal Z")
            {   gimbal_links=llListReplaceList(
                    gimbal_links,[index],2,2);
            }else if(llGetLinkName(index)=="Base")
            {   object_links=llListReplaceList(
                    object_links,[index],0,0);
            }else if(llGetLinkName(index)=="UP")
            {   object_links=llListReplaceList(
                    object_links,[index],1,1);
            }else if(llGetLinkName(index)=="DWN")
            {   object_links=llListReplaceList(
                    object_links,[index],2,2);
            }else if(llGetLinkName(index)=="LFT")
            {   object_links=llListReplaceList(
                    object_links,[index],3,3);
            }else if(llGetLinkName(index)=="RGH")
            {   object_links=llListReplaceList(
                    object_links,[index],4,4);
            }else if(llGetLinkName(index)=="FWD")
            {   object_links=llListReplaceList(
                    object_links,[index],5,5);
            }else if(llGetLinkName(index)=="BCK")
            {   object_links=llListReplaceList(
                    object_links,[index],6,6);
            }
        }
        llSetText("",<0.0,0.0,0.0>,0.0);
    }

    touch_start(integer total_number)
    {
        if(llDetectedLinkNumber(0)==LINK_ROOT)
        {   uSetSubRot(
                object_links,
                object_offsets,
                object_rotations,
                gRotBase=<0,0,0,1>
            );
        }
        start_angle=uGetGimbalAngle();
        
    }
    touch(integer i)
    {
        vector now_angle = uGetGimbalAngle();
        rotation rot = llEuler2Rot(now_angle-start_angle)*gRotBase;
        uSetSubRot(
            object_links,
            object_offsets,
            object_rotations,
            rot
        );
        llSetText("Rotation:\n"+
            (string)(rot)+"\n"+
            "XYZ intrinsic:\n"+
            (string)(uRot2Euler(rot)*RAD_TO_DEG)+
            "\nZYX intrinsic:\n"+
            (string)(uRot2Euler2(rot)*RAD_TO_DEG)
        ,<0,1.0,0>,1.0);
        //llSetText("Rotation:\n"+ //comment out settext above and uncomment this for extrinsic.
        //    (string)(rot)+"\n"+
        //    "ZYX extrinsic:\n"+
        //    (string)(uRot2Euler(rot)*RAD_TO_DEG)+
        //    "\nXYZ extrinsic:\n"+
        //    (string)(uRot2Euler2(rot)*RAD_TO_DEG)
        //,<0,1.0,0>,1.0);
        
    }
    touch_end(integer i)
    {
        vector now_angle = uGetGimbalAngle();
        gRotBase=llEuler2Rot(now_angle-start_angle)*gRotBase;

        uSetSubRot( // comment this out for extrinsic rotation.
            gimbal_links,
            gimbal_offsets,
            gimbal_rotations,
            gRotBase
        );
    }
}

 

  • Thanks 2
Link to comment
Share on other sites

For my own sanity I'll leave that last example uncorrected, but uRot2Euler was intended to function like llRot2Euler, the correct implementation being:

vector uRot2Euler(rotation rot)
{
   vector euler;
   vector temp;
   // x-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.x = llAtan2(temp.z, temp.y) - PI_BY_TWO;
   rot /= llEuler2Rot(<euler.x, 0.0, 0.0>);
   // y-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.y = -llAtan2(temp.z, temp.x) + PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.y, 0.0>);
   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.z = llAtan2(temp.y, temp.x);
   return euler;
}

 

  • Thanks 3
Link to comment
Share on other sites

Looking at this again. I forgot to add a comment in that the touch-grab gimbal example, to modify it to use global rotations, you also need to replace

gRotBase=llEuler2Rot(now_angle-start_angle)*gRotBase;

with

gRotBase=gRotBase*llEuler2Rot(now_angle-start_angle);

in both the touch and touch_end events.

Link to comment
Share on other sites

  • 3 months later...

another useful thing:

rotate_prim_intrinsic(integer link,vector axis, float angle, vector offset)
{
    list l = llGetLinkPrimitiveParams(link,[PRIM_POS_LOCAL,PRIM_ROT_LOCAL]);
    vector local_pos = llList2Vector(l,0);
    rotation local_rot = llList2Rot(l,1);
    rotation rot = llAxisAngle2Rot(axis,angle);
    
    vector offset2 = offset*(rot);
    llSetLinkPrimitiveParamsFast(link,
    [   PRIM_POS_LOCAL,local_pos+(offset2-offset)*local_rot,
        PRIM_ROT_LOCAL,rot*local_rot
    ]);
}

if you have a prim, and you want to rotate it about its own local axes around a point not its center, can use the above function to do just that.

For example, to rotate a flat cube (e.g. a door) with dimensions  x:0.5,y:0.01,z:(any) 30 degrees about its -x side:

rotate_prim_intrinsic(link_number,<0,0,1>,PI_BY_TWO/3,<-0.25,0,0>);
  • Thanks 2
Link to comment
Share on other sites

  • 7 months later...

It turns out converting rotations into Euler-vectors in strange orders is non-trivial.

I hope nobody ever has to actually use these for anything serious. . .

vector uRot2XYZ(rotation rot)
{
   vector euler;
   vector temp;
   
   // x-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.x = llAtan2(temp.z, temp.y) - PI_BY_TWO;
   rot /= llEuler2Rot(<euler.x, 0.0, 0.0>);
   
   // y-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.y = -llAtan2(temp.z, temp.x) + PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.y, 0.0>);
   
   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.z = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.z>);
   
   return euler;
}
vector uRot2XZY(rotation rot)
{
   vector euler;
   vector temp;
   
   // x-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.x = llAtan2(temp.z, temp.y);
   rot /= llEuler2Rot(<euler.x, 0.0, 0.0>);
   
   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.y = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.y>);
   
   // y-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.z = llAtan2(temp.x, temp.z);
   rot /= llEuler2Rot(<0.0, euler.z, 0.0>);
   
   return euler;

}
vector uRot2YXZ(rotation rot)
{
   vector euler;
   vector temp;
   
   // y-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.x = llAtan2(temp.x, temp.z);
   rot /= llEuler2Rot(<0.0, euler.x, 0.0>);
   
   // x-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.y = llAtan2(temp.z, temp.y);
   rot /= llEuler2Rot(<euler.y, 0.0, 0.0>);
   
   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.z = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.z>);

   return euler;

}
vector uRot2YZX(rotation rot)
{
   vector euler;
   vector temp;

   // y-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.x = llAtan2(temp.x, temp.z)-PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.x, 0.0>);
    
   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.y = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.y>);
   
   // x-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.z = llAtan2(temp.z, temp.y) - PI_BY_TWO;
   rot /= llEuler2Rot(<euler.z, 0.0, 0.0>);
   
   return euler;

}
vector uRot2ZXY(rotation rot)
{
   vector euler;
   vector temp;

   // z-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.x = llAtan2(temp.y, temp.x)-PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, 0.0, euler.x>);
   
   // x-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.y = llAtan2(temp.z, temp.y);
   rot /= llEuler2Rot(<euler.y, 0.0, 0.0>);
   
   // y-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.z = llAtan2(temp.x, temp.z)-PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.z, 0.0>);

   return euler;

}
vector uRot2ZYX(rotation rot)
{
   vector euler;
   vector temp;

   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.x = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.x>);
   
   // y-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.y = llAtan2(temp.x, temp.z) - PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.y, 0.0>);
   
   // x-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.z = llAtan2(temp.z, temp.y) - PI_BY_TWO;
   rot /= llEuler2Rot(<euler.z, 0.0, 0.0>);
   
   return euler;
}
vector uRot2XYX(rotation rot)
{
   vector euler;
   vector temp;
   // x-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.x = llAtan2(temp.z, temp.y)-PI_BY_TWO;
   rot /= llEuler2Rot(<euler.x, 0.0, 0.0>);
   
   // y-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.y = llAtan2(temp.x, temp.z) - PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.y, 0.0>);
   
   // x-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.z = llAtan2(temp.z, temp.y);
   rot /= llEuler2Rot(<euler.z, 0.0, 0.0>);
   
   return euler;
}
vector uRot2XZX(rotation rot)
{
   vector euler;
   vector temp;

   // x-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.x = llAtan2(temp.z, temp.y);
   rot /= llEuler2Rot(<euler.x, 0.0, 0.0>);
   
   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.y = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.y>);
   
   // x-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.z = llAtan2(temp.z, temp.y);
   rot /= llEuler2Rot(<euler.z, 0.0, 0.0>);
   
   return euler;
}
vector uRot2YXY(rotation rot)
{
   vector euler;
   vector temp;

   // y-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.x = llAtan2(temp.x, temp.z);
   rot /= llEuler2Rot(<0.0, euler.x, 0.0>);
   
   // x-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.y = llAtan2(temp.z, temp.y);
   rot /= llEuler2Rot(<euler.y, 0.0, 0.0>);
   
   // y-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.z = llAtan2(temp.x, temp.z)-PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.z, 0.0>);
   
   return euler;
}
vector uRot2YZY(rotation rot)
{
   vector euler;
   vector temp;

   // y-axis
   // interestingly, -PI_BY_TWO results in a different valid answer, the check o which is a negative quaternion.
   temp = <0.0, 1.0, 0.0> * rot;
   euler.x = llAtan2(temp.x, temp.z)+PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.x, 0.0>);
   
   // z-axis
   temp = <0.0, 1.0, 0.0> * rot;
   euler.y = llAtan2(temp.y, temp.x)-PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, 0.0, euler.y>);
   
   // y-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.z = llAtan2(temp.x, temp.z)-PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, euler.z, 0.0>);
   
   return euler;
}
vector uRot2ZXZ(rotation rot)
{
   vector euler;
   vector temp;
   // z-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.x = llAtan2(temp.y, temp.x)-PI_BY_TWO;
   rot /= llEuler2Rot(<0.0, 0.0, euler.x>);
   
   // x-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.y = llAtan2(temp.z, temp.y)-PI_BY_TWO;
   rot /= llEuler2Rot(<euler.y, 0.0, 0.0>);
   
   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.z = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.z>);
   
   return euler;
}
vector uRot2ZYZ(rotation rot)
{
   vector euler;
   vector temp;

   // z-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.x = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.x>);
   
   // Y-axis
   temp = <0.0, 0.0, 1.0> * rot;
   euler.y = llAtan2(temp.x, temp.z);
   rot /= llEuler2Rot(<0.0, euler.y, 0.0>);
   
   // z-axis
   temp = <1.0, 0.0, 0.0> * rot;
   euler.z = llAtan2(temp.y, temp.x);
   rot /= llEuler2Rot(<0.0, 0.0, euler.z>);
   
   return euler;
}
default
{
    state_entry()
    {
        rotation r = <0.5,0.4,0.3,0.7071>;
        rotation check;
        
        vector XYZ = uRot2XYZ(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<0,0,1>,XYZ.z)*
            llAxisAngle2Rot(<0,1,0>,XYZ.y)*
            llAxisAngle2Rot(<1,0,0>,XYZ.x);
        llOwnerSay(llList2CSV(["XYZ",RAD_TO_DEG*XYZ,check]));
        
        vector XZY = uRot2XZY(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<0,1,0>,XZY.z)*
            llAxisAngle2Rot(<0,0,1>,XZY.y)*
            llAxisAngle2Rot(<1,0,0>,XZY.x);
        llOwnerSay(llList2CSV(["XZY",RAD_TO_DEG*XZY,check]));
        
        vector YXZ = uRot2YXZ(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<0,0,1>,YXZ.z)*
            llAxisAngle2Rot(<1,0,0>,YXZ.y)*
            llAxisAngle2Rot(<0,1,0>,YXZ.x);
        llOwnerSay(llList2CSV(["YXZ",RAD_TO_DEG*YXZ,check]));
        
        vector YZX = uRot2YZX(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<1,0,0>,YZX.z)*
            llAxisAngle2Rot(<0,0,1>,YZX.y)*
            llAxisAngle2Rot(<0,1,0>,YZX.x);
        llOwnerSay(llList2CSV(["YZX",RAD_TO_DEG*YZX,check]));
        
        vector ZXY = uRot2ZXY(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<0,1,0>,ZXY.z)*
            llAxisAngle2Rot(<1,0,0>,ZXY.y)*
            llAxisAngle2Rot(<0,0,1>,ZXY.x);
        llOwnerSay(llList2CSV(["ZXY",RAD_TO_DEG*ZXY,check]));
        
        vector ZYX = uRot2ZYX(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<1,0,0>,ZYX.z)*
            llAxisAngle2Rot(<0,1,0>,ZYX.y)*
            llAxisAngle2Rot(<0,0,1>,ZYX.x);
        llOwnerSay(llList2CSV(["ZYX",RAD_TO_DEG*ZYX,check]));
        
        vector XYX = uRot2XYX(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<1,0,0>,XYX.z)*
            llAxisAngle2Rot(<0,1,0>,XYX.y)*
            llAxisAngle2Rot(<1,0,0>,XYX.x);
        llOwnerSay(llList2CSV(["XYX",RAD_TO_DEG*XYX,check]));
        
        vector XZX = uRot2XZX(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<1,0,0>,XZX.z)*
            llAxisAngle2Rot(<0,0,1>,XZX.y)*
            llAxisAngle2Rot(<1,0,0>,XZX.x);
        llOwnerSay(llList2CSV(["XZX",RAD_TO_DEG*XZX,check]));
        
        vector YXY = uRot2YXY(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<0,1,0>,YXY.z)*
            llAxisAngle2Rot(<1,0,0>,YXY.y)*
            llAxisAngle2Rot(<0,1,0>,YXY.x);
        llOwnerSay(llList2CSV(["YXY",RAD_TO_DEG*YXY,check]));

        vector YZY = uRot2YZY(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<0,1,0>,YZY.z)*
            llAxisAngle2Rot(<0,0,1>,YZY.y)*
            llAxisAngle2Rot(<0,1,0>,YZY.x);
        llOwnerSay(llList2CSV(["YZY",RAD_TO_DEG*YZY,check]));
        
        vector ZXZ = uRot2ZXZ(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<0,0,1>,ZXZ.z)*
            llAxisAngle2Rot(<1,0,0>,ZXZ.y)*
            llAxisAngle2Rot(<0,0,1>,ZXZ.x);
        llOwnerSay(llList2CSV(["ZXZ",RAD_TO_DEG*ZXZ,check]));

        vector ZYZ = uRot2ZYZ(r);
        check = // linden rots are reverse from math rots.
            llAxisAngle2Rot(<0,0,1>,ZYZ.z)*
            llAxisAngle2Rot(<0,1,0>,ZYZ.y)*
            llAxisAngle2Rot(<0,0,1>,ZYZ.x);
        llOwnerSay(llList2CSV(["ZYZ",RAD_TO_DEG*ZYZ,check]));
    }
}
  • Thanks 2
Link to comment
Share on other sites

  • 2 months later...
On 5/18/2022 at 6:24 PM, Quistess Alpha said:

It turns out converting rotations into Euler-vectors in strange orders is non-trivial.

I hope nobody ever has to actually use these for anything serious. . .

If you have ever had to deal with BVH files, those routines would be VERY useful. BVH files can use arbitrary rotation orders, and these routines are something I had to ask for help with many years back... and that was just for a single variation of rot to a specific order.

Thanks for pointing out the LL reverse order and the check routines that also show a better way to construct a rot from arbitrarily ordered Eulers!

BTW, is it LL is using a reverse order, or is it that evaluation order is right to left?

 

Link to comment
Share on other sites

2 minutes ago, Phate Shepherd said:

BTW, is it LL is using a reverse order, or is it that evaluation order is right to left?

Quaternions are associative. I.E. if you have 3 quaternions p,q,r :

((p*q)*r) == (p*(q*r)); in any valid quaternion implementation. The difference really is that in LSL, multiplication is defined mirror-image with respect to well, literally anything else.

In LSL, the product of 2 quaternions q*r roughly means "Rotate by q, then by r (in region coordinates)", r is a transformation applied to q. In math land, it's flipped around. q*r means "Rotate by r then by q", q is a transformation applied to r, which "makes more sense" if you think of a quaternion/rotation not as a static thing, but as a function, where you usually write the function name on the left (i.e. f(x) ). but that's a lot of semantics.

 

  • Like 3
Link to comment
Share on other sites

  • 1 month later...

Another "maybe occasionally useful" thing to put on the pile: getting the bounding box of an arbitrarily oriented cube rectangular cuboid:

Quote
vector BoxRot2AABox(vector b, rotation r)
{   //b = 0.5*b; // offset from center is more useful than actual dimension.
    // it turns out scaling cancels itself out and becomes redundant.
    list corners = 
    [   < b.x, b.y, b.z >*r,
        < b.x, b.y,-b.z >*r,
        < b.x,-b.y, b.z >*r,
        < b.x,-b.y,-b.z >*r
        //<-b.x, b.y, b.z >*r, // symmetry.
        //<-b.x, b.y,-b.z >*r,
        //<-b.x,-b.y, b.z >*r,
        //<-b.x,-b.y,-b.z >*r
    ];
    vector max;
    integer index = -llGetListLength(corners);
    do
    {   vector c = llList2Vector(corners,index);
        c.x = llFabs(c.x);
        c.y = llFabs(c.y);
        c.z = llFabs(c.z);
        if(c.x>max.x) max.x = c.x;
        if(c.y>max.y) max.y = c.y;
        if(c.z>max.z) max.z = c.z;
    }while(++index);
    return max;
}
// https://wiki.secondlife.com/wiki/LlSetKeyframedMotion
rotation NormRot(rotation Q)
{   float MagQ = llSqrt(Q.x*Q.x + Q.y*Q.y +Q.z*Q.z + Q.s*Q.s);
    // MagQ = 1/sqrt... and return <Q.x*MagQ ... would probably be slightly more efficient.
    return <Q.x/MagQ, Q.y/MagQ, Q.z/MagQ, Q.s/MagQ>;
}
default
{   touch_start(integer total_number)
    {   vector box = 
           <0.1+llFrand(0.9),
            0.1+llFrand(0.9),
            0.1+llFrand(0.9)>;
        // no clue how "even" a distribution this generates.
        rotation rot = NormRot(
           <1.0-llFrand(2.0),
            1.0-llFrand(2.0),
            1.0-llFrand(2.0),
            1.0-llFrand(2.0)>);
        llSetLinkPrimitiveParamsFast(2,
        [   PRIM_SIZE, box,
            PRIM_ROT_LOCAL, rot,
            PRIM_LINK_TARGET, 1,
            PRIM_SIZE, BoxRot2AABox(box,rot)
        ]);
    }
}

 

The general technique could easily be extended to any specific convex hull specified in terms of its scale.

Edited by Quistess Alpha
Link to comment
Share on other sites

  • 4 months later...

I wasn't sure whether to make a new topic for this, but it's basically just applied rotation stuff so I'll dump it here.

sun_position_user returns azimuth and elevation values to plug into the environment editor (you'll have to do the math to convert a real day of the year into my fantasy 360 day calendar :P )

sun_position_environment returns a rotation you can plug into llSetAgentEnvironment

float gFrequency = 2.0; // 2.0 seems about the fastest it will allow
float gLatitude = 50.0; // degrees from equator.
integer gTime=-1;
integer gDay;
vector sun_position_user(float latitude, float day, float time)
{   // day ranges from [0-360); day 0 is september equinox, 90 winter solstice, 270 summer solstice
    // latitude in degrees from equator
    // time in hours; exactly 24 hours per day. (sun rises at 6.0 on the equinox, sets at 18.0)
    // return is <azimuth, elevation, 0>. (in degrees)
    latitude*= DEG_TO_RAD;
    vector axis = <0, llCos(latitude), llSin(latitude)>;
    //vector sun = <0, -axis.z, axis.y> + axis*(llSin(day*DEG_TO_RAD)*0.3987); 
    vector sun = <0, axis.z, -axis.y> * 
        llAxisAngle2Rot(<-1,0,0>,0.4101524*llSin(day*DEG_TO_RAD)) *
        llAxisAngle2Rot(axis,TWO_PI*-time/24.0);
    // .4101524 is the tilt of the earth's axis in radians.
    // sun now represents the position of the sun which we want to convert to az, elevation.
    vector ret = RAD_TO_DEG*<llAtan2(sun.y,sun.x),llAtan2(sun.z,llVecMag(<sun.x,sun.y,0>)),0>;
    if(ret.x<0) ret.x+=360.0;
    return ret;
}
rotation sun_position_environment(float latitude, float day, float time)
{   latitude*= DEG_TO_RAD;
    vector axis = <0, llCos(latitude), llSin(latitude)>;
    //llOwnerSay("Axis:"+(string)axis);
    vector sun = <0, axis.z, -axis.y> * 
        llAxisAngle2Rot(<-1,0,0>,0.4101524*llSin(day*DEG_TO_RAD)) *
        llAxisAngle2Rot(axis,TWO_PI*-time/24.0);

    //llOwnerSay("Sun:"+(string)sun+":"+(string)time);
    return uVectorRoll2Rot(sun,0.0);
}
rotation uVectorRoll2Rot(vector v,float r)
{   return YPR2Rot(<llAtan2(v.y,v.x),-llAtan2(v.z,llVecMag(<v.x,v.y,0>)),r>);
}
rotation YPR2Rot(vector v) // Yaw Pitch Roll
{   return
        <llSin(v.z/2),0,0,llCos(v.z/2)> *
        <0,llSin(v.y/2),0,llCos(v.y/2)> *
        <0,0,llSin(v.x/2),llCos(v.x/2)> ;
}

default
{
    state_entry()
    {   llSetTimerEvent(gFrequency);
        llOwnerSay("Script start.");
    }
    timer()
    {   ++gTime;
        if(gTime==24)
        {   gTime=0;
            ++gDay;
        }
        list agents = llGetAgentList(AGENT_LIST_PARCEL,[]);
        integer i = llGetListLength(agents);
        while(~--i)
        {   llRequestExperiencePermissions(llList2Key(agents,i),"");
        }
    }
    experience_permissions(key ID)
    {   llSetAgentEnvironment(ID,gFrequency*0.95,[SKY_SUN,sun_position_environment(gLatitude,gDay,gTime),1.0,<0.1,0.1,0.1>]);
        // Update does not happen if the receiving viewer is in the middle of an environment change, so env. change speed must be faster than update frequency.
        //llOwnerSay((string)sun_position_user(gLatitude,gDay,gTime));
    }
    experience_permissions_denied(key ID, integer reason)
    {   llOwnerSay(llGetExperienceErrorMessage(reason));
    }
}

 

  • Thanks 1
Link to comment
Share on other sites

  • 4 months later...

More applied rotation things: fudging Omega and 'real' orientations to play nice with each-other for a car's wheels:

// this script expects a linkset with 2 links: a cube root prim, and a cylinder prim wheel. controls by touching the faces of the cube.

// for the rest of this script and comments, 'roll' means what a wheel does when going forward or backwards , 'turn' means what a wheel does when moving left or right, not around its main axis.

//current wheel state:
float gSpeedRoll = 0.0; // how fast the wheel is turning forwards or backwards
float gAngleTurn = 0.0; // in radians: how far the wheel is turned left or right.

// wheel intrinsic parameters:
// llGetLinkPrimitiveParams(wheel,[PRIM_ROT_LOCAL]); when the wheel is facing forward in default 'rest position':
rotation gWheelRotIntrinsic = <-0.5,0.5,0.5,0.5>;
// with 'snap: local' checked in the edit window, and with the root prim (or the entire linkset) selected, this axis points left (from the perspective of the vehicle/object):
vector   gAxisRoll = <0,1,0>;
// when selected as above, this axis points directly up at the sky:
vector   gAxisTurn = <0,0,1>;

set_speed(float s)
{   llSetLinkPrimitiveParamsFast(2,
    [   PRIM_OMEGA, gAxisRoll*llAxisAngle2Rot(gAxisTurn,gAngleTurn), s, 1.0
    ]);
}
set_turn(float r)
{   llSetLinkPrimitiveParamsFast(2,
    [   PRIM_ROT_LOCAL, 
            gWheelRotIntrinsic*
            llAxisAngle2Rot(gAxisRoll,estimate_theta())* // attempt to fix wheel not rolling while turning. 
            llAxisAngle2Rot(gAxisTurn,r),
        PRIM_OMEGA, gAxisRoll*llAxisAngle2Rot(gAxisTurn,gAngleTurn), gSpeedRoll, 1.0
    ]);
}
float gTheta; // angle of how much the wheel has rolled.
float estimate_theta()
{   gTheta += llGetAndResetTime()*gSpeedRoll;
    gTheta -= TWO_PI*llFloor(gTheta/TWO_PI); // wrap to [0 thru TWO_PI)
    return gTheta;
}

default
{
    state_entry()
    {   llSetLinkPrimitiveParamsFast(2,
        [   PRIM_ROT_LOCAL, gWheelRotIntrinsic,
            PRIM_OMEGA, <0,0,1>, 0.0, 0.0 
        ]);
        llMinEventDelay(0.2); // to make the touch() event behave performantly.
    }

    touch_start(integer total_number)
    {   integer face = llDetectedTouchFace(0);
        estimate_theta(); // reset theta before changing gSpeedRoll
        if(2==face) set_speed(gSpeedRoll+=0.25);
        else if(4==face) set_speed(gSpeedRoll-=0.25);
    }
    touch(integer n)
    {   integer face = llDetectedTouchFace(0);
        if(3==face) set_turn(gAngleTurn+=0.125);
        else if(1==face) set_turn(gAngleTurn-=0.125);
    }
}

see

 

Edited by Quistess Alpha
  • Like 1
Link to comment
Share on other sites

  • 9 months later...
Posted (edited)

I thought I mentioned this in my rambles above, but I'm not finding it in a brief skim, and it's conceptually important:

Vectors can represent both positions in-world, and global changes to position. To 'apply' a change to a position in-world, you simply add a change vector to a position vector, and set the position of something to that new position. For example:

llSetPos(llGetPos()+<1,0,0>); // move 1 unit in the x-direction.

Rotations can be used in a similar way, but with a few important differences:

  • rotations are less trivial to express on their own. One of the best ways of creating a rotation 'from scratch' is to use llAxisAngle2Rot()
  • rotations are multiplied instead of added together.
  • the order of multiplication matters.

that last point can be confusing at first, but once you understand what each order does, it can be very useful: In LSL, when you multiply in the order orientation*change, you rotate around the world x,y,z axes (like you would see when editing an object with 'snap world' checked); when you multiply in the order change*orientation, you rotate around the object's intrinsic axes (like you would see when editing an object with 'snap local' checked)

As a practical example, say you're scripting something like a turret that can aim up and down and move left and right. To aim up you might use something like

llSetRot(llAxisAngle2Rot(<0,1,0>,-15*DEG_TO_RAD) * llGetRot() ); // apply local rotation on y axis

to move the gun up around its y axis, independent of whatever direction it's facing, but to aim left/right you would use:

llSetRot(llGetRot() * llAxisAngle2Rot(<0,0,1>, 15* DEG_TO_RAD) ); // apply global rotation on z axis

Of course, for an actual turret, you might want to apply the rotations to different links, but the same principals apply.

Edited by Quistess Alpha
  • Thanks 1
Link to comment
Share on other sites

  • 2 weeks later...
Posted (edited)

For SL at least "standard orientation" for objects is for them to be "facing" the +x direction, with +z being up and +y being left.

When faced with a situation where the object you're working with isn't "right side up" and "facing" due east when its orientation is set to ZERO_ROTATION, I usually suggest just making it so: link your object to an invisible root prim, and rotate it so that the child prim is in standard orientation. It saves a lot of headaches.

That said, if you ~absolutely must work with an object in non-standard orientation, you will probably need to determine its "intrinsic rotation" which I'll call rINTRINSIC. Rotate your object so that it's in standard orientation, and confirm that when the object is selected in edit mode, with the 'move' radio button on and "snap: World" checked,  the red arrow and the object point in the same direction and the blue arrow points "up". Then, you can use the three rotation numbers in the object tab of the build window to set rINTRINSIC, and use that to find the axes of the object that point forward, up and left:

rotation rINTRINSIC;

vector UP;
vector FWD;
vector LEFT;

default
{   state_entry()
    {   rINTRINSIC = llEuler2Rot(DEG_TO_RAD*<0,270,0>); // X,Y,Z numbers from the build menu.
        UP = <0,0,1>/rINTRINSIC;
        FWD = <1,0,0>/rINTRINSIC;
        LEFT = <0,1,0>/rINTRINSIC;
    }
}

Preferably though, you should set rINTRINSIC based on the axes of the object. When the object is in standard orientation as above and if your object has a 'sane' intrinsic orientation, changing between snap:world and snap:local will only change the colors of the arrows, the local axes are still aligned with the world axes.

rotation rINTRINSIC;

// which local arrow points up?
vector UP = <1,0,0>; // for example, the red arrow (representing <1,0,0> or +x) in snap:local points in the same direction as the blue arrow in snap:world, up.

// which local arrow points forward? for an animal, which arrow points with its eyes, for a vehicle, where would it move?
vector FWD = <0,0,-1>; // for example, the blue arrow (+z) in snap:local is aligned with the red arrow in snap:world, but points opposite direction (so add a minus sign). 

// which local arrow points left?
vector LEFT = <0,1,0>; // for example, the green arrow (+y) is exactly the same between snap:world and snap:local.

default
{   state_entry()
    {   rINTRINSIC = llAxes2Rot(FWD,LEFT,UP);
    }
}

With those values in-hand, here are a few functions that might be helpful. (I'm writing them as functions for clarity, but you can of course in-line the simpler ones)

vector dirFacing() // in global coordinates, what direction is this object 'facing'?
{   return FWD*llGetRot();
}
integer WhichSideIsUp() // 0 for 'right-side-up, 1 for upside-down, based on the value of global variable UP
{   return ( (UP*llGetRot())*<0,0,1> ) < 0;
    // more reasonable than testing the object's roll.
}
integer AmLookingDown() // 1 for looking below horizon, 0 for looking up
{   return ( (FWD*llGetRot())*<0,0,1> ) < 0;
}

uSetRotYPR(vector v) // 'Yaw', 'Pitch' and 'Roll', with respect to rINTRINSIC
{   
    rotation ypr = 
        llAxisAngle2Rot(<1,0,0>,v.z) *
        llAxisAngle2Rot(<0,1,0>,v.y) *
        llAxisAngle2Rot(<0,0,1>,v.x);
    llSetLinkPrimitiveParamsFast(0,
    [   PRIM_ROTATION, (ZERO_ROTATION/rINTRINSIC)*ypr
    ]);
}

vector uGetRotYPR()
{   rotation q = rINTRINSIC*llGetRot();
// https://community.secondlife.com/forums/topic/471561-urot2euler/?do=findComment&comment=2293046
    vector v;
    // x-axis
    float s = 2 * (q.s * q.x + q.y * q.z);
    float c = 1 - 2 * (q.x * q.x + q.y * q.y);
    v.z = llAtan2(s, c);
    // y-axis
    s = 2 * (q.s * q.y - q.z * q.x);
    if (llFabs(s) >= 1)
        v.y = (-1 | (s < 0)) * PI_BY_TWO;
    else
        v.y = llAsin(s);
    // z-axis
    s = 2 * (q.s * q.z + q.x * q.y);
    c = 1 - 2 * (q.y * q.y + q.z * q.z);
    v.x = llAtan2(s, c);
    return v;
}

uLookAt(vector direction, float roll) // to look at a specific position in-world, set direction to (position-llGetPos())
{   rotation look = // inline YPR2Rot() 
        llAxisAngle2Rot(<1,0,0>, roll) *
        llAxisAngle2Rot(<0,1,0>, 
            -llAtan2(direction.z,llVecMag(<direction.x,direction.y,0>)) )*
        llAxisAngle2Rot(<0,0,1>, llAtan2(direction.y,direction.x) ) ;
        
    llSetLinkPrimitiveParamsFast(0,
    [   PRIM_ROTATION, (ZERO_ROTATION/rINTRINSIC)*look
    ]);
}

 

Edited by Quistess Alpha
  • Thanks 2
Link to comment
Share on other sites

  • 2 weeks later...

Texture animation counts as rotation, right?

// public domain texture-animation 'tick' script.
// This script only works correctly when the rotated face has 0 offset and 0 rotation degrees and 1 Horizontal scale and 1 Vertical scale in the build window! (texture animation is buggy.)

integer face = 0; // which face to rotate, or ALL_SIDES
float degrees = 30.0; // how much to rotate each tick.
float time = 0.75; // how long to rotate each tick
float time_tick = 1.0; // how long is each tick in total (time+pause)
integer tick_max=12; // if non-zero, only do this many ticks.

integer tick=-1;

default
{   state_entry()
    {   //llSetTexture("571d44bb-8e5f-a579-eaec-cdb0bd109b29",face);
        degrees = DEG_TO_RAD*degrees;
        llSetTimerEvent(time_tick);
        if(tick_max) llSay(0,"Time starts now!");
    }
    timer()
    {   ++tick;
        
        // end condition check:
        if(tick_max && (tick>=tick_max))
        {   llSetTextureAnim( ROTATE, face, 0,0,0,0,0); // SMOOTH bit on causes less smoothness than without it!
                                                        // but having no bits set returns the face back to 0.
            llSetTimerEvent(0.0);
            llSay(0,"Time is up!");
            return;
        }

        // do the texture animation:
            // must stop previous anim before starting the next one, or else animation will be instant:
        llSetTextureAnim( ROTATE, face, 0,0,0,0,0); // smooth bit on causes less smoothness than without it!
        llSleep(0.05); // 0.05 should always work, 0.0233 works 75% of ticks or so.
        llSetTextureAnim(ANIM_ON|SMOOTH|ROTATE, face, 0, 0, 
            (tick*degrees) , 
            1+(degrees) ,
            degrees/time);
    }
}

 

  • Thanks 1
Link to comment
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
 Share

×
×
  • Create New...