Quistess Alpha Posted June 2, 2021 Share Posted June 2, 2021 (edited) 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 June 2, 2021 by Quistessa sudden epiphany for RotInterp. 3 3 Link to comment Share on other sites More sharing options...
Quistess Alpha Posted June 2, 2021 Author Share Posted June 2, 2021 -- 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 ); } } 2 Link to comment Share on other sites More sharing options...
Quistess Alpha Posted June 2, 2021 Author Share Posted June 2, 2021 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; } 3 Link to comment Share on other sites More sharing options...
Quistess Alpha Posted June 9, 2021 Author Share Posted June 9, 2021 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 More sharing options...
Quistess Alpha Posted September 28, 2021 Author Share Posted September 28, 2021 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>); 2 Link to comment Share on other sites More sharing options...
Quistess Alpha Posted May 18, 2022 Author Share Posted May 18, 2022 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])); } } 2 Link to comment Share on other sites More sharing options...
Phate Shepherd Posted July 19, 2022 Share Posted July 19, 2022 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 More sharing options...
Quistess Alpha Posted July 19, 2022 Author Share Posted July 19, 2022 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. 3 Link to comment Share on other sites More sharing options...
Quistess Alpha Posted August 25, 2022 Author Share Posted August 25, 2022 (edited) 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 August 25, 2022 by Quistess Alpha Link to comment Share on other sites More sharing options...
Quistess Alpha Posted January 10 Author Share Posted January 10 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 ) 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)); } } 1 Link to comment Share on other sites More sharing options...
Bugs Larnia Posted January 15 Share Posted January 15 From the rotationally challenged among us: thank you! 1 Link to comment Share on other sites More sharing options...
Recommended Posts
Please take a moment to consider if this thread is worth bumping.
Please sign in to comment
You will be able to leave a comment after signing in
Sign In Now