Jump to content

Local to global rotations - that old chestnut!


dafydd99

Recommended Posts

Thank you, dafydd99, for your papyrus code related to rotation matrix.

 

In this context I want to reflect to wiki page https://www.creationkit.com/index.php?title=Spatial_functions_and_snippets, which is a mirror from an old bethesda forum posting made by chesko.

 

GetPosXYZRotateAroundRef()

 

;-----------\
;Description \ Author: Chesko
;----------------------------------------------------------------
; https://www.creationkit.com/index.php?title=Spatial_functions_and_snippets
;Rotates a point (akObject offset from the center of
;rotation (akOrigin) by the supplied degrees fAngleX, fAngleY,
;fAngleZ, and returns the new position of the point.
 
;-------------\
;Return Values \
;----------------------------------------------------------------
;               fNewPos[0]      =        The new X position of the point
;               fNewPos[1]      =        The new Y position of the point
;               fNewPos[2]      =        The new Z position of the point
 
;                        |  1                    0                0     |
;Rx(t) =                 |  0                   cos(t)         -sin(t)  |
;                        |  0                   sin(t)          cos(t)  |
;
;                        | cos(t)                0              sin(t)  |
;Ry(t) =                 |  0                    1                0     |
;                        |-sin(t)                0              cos(t)  |
;
;                        | cos(t)              -sin(t)            0     |
;Rz(t) =                 | sin(t)               cos(t)            0     |
;                        |  0                    0                1     |
 
;R * v = Rv, where R = rotation matrix, v = column vector of point [ x y z ], Rv = column vector of point after rotation
;Provided angles must follow Bethesdas conventions (CW Z angle for example).
float[] function GetPosXYZRotateAroundRef(ObjectReference akOrigin, ObjectReference akObject, float fAngleX, float fAngleY, float fAngleZ)
        fAngleX = -(fAngleX)
        fAngleY = -(fAngleY)
        fAngleZ = -(fAngleZ)
 
        float myOriginPosX = akOrigin.GetPositionX()
        float myOriginPosY = akOrigin.GetPositionY()
        float myOriginPosZ = akOrigin.GetPositionZ()
 
        float fInitialX = akObject.GetPositionX() - myOriginPosX
        float fInitialY = akObject.GetPositionY() - myOriginPosY
        float fInitialZ = akObject.GetPositionZ() - myOriginPosZ
 
        float fNewX
        float fNewY
        float fNewZ
 
        ;Objects in Skyrim are rotated in order of Z, Y, X, so we will do that here as well.
 
        ;Z-axis rotation matrix
        float fVectorX = fInitialX
        float fVectorY = fInitialY
        float fVectorZ = fInitialZ
        fNewX = (fVectorX * cos(fAngleZ)) + (fVectorY * sin(-fAngleZ)) + (fVectorZ * 0)
        fNewY = (fVectorX * sin(fAngleZ)) + (fVectorY * cos(fAngleZ)) + (fVectorZ * 0)
        fNewZ = (fVectorX * 0) + (fVectorY * 0) + (fVectorZ * 1)        
 
        ;Y-axis rotation matrix
        fVectorX = fNewX
        fVectorY = fNewY
        fVectorZ = fNewZ
        fNewX = (fVectorX * cos(fAngleY)) + (fVectorY * 0) + (fVectorZ * sin(fAngleY))
        fNewY = (fVectorX * 0) + (fVectorY * 1) + (fVectorZ * 0)
        fNewZ = (fVectorX * sin(-fAngleY)) + (fVectorY * 0) + (fVectorZ * cos(fAngleY))
 
        ;X-axis rotation matrix
        fVectorX = fNewX
        fVectorY = fNewY
        fVectorZ = fNewZ        
        fNewX = (fVectorX * 1) + (fVectorY * 0) + (fVectorZ * 0)
        fNewY = (fVectorX * 0) + (fVectorY * cos(fAngleX)) + (fVectorZ * sin(-fAngleX))
        fNewZ = (fVectorX * 0) + (fVectorY * sin(fAngleX)) + (fVectorZ * cos(fAngleX))
 
        ;Return result
        float[] fNewPos = new float[3]
        fNewPos[0] = fNewX + myOriginPosX
        fNewPos[1] = fNewY + myOriginPosY
        fNewPos[2] = fNewZ + myOriginPosZ
        return fNewPos
endFunction

 

 

 

And as second hint, it is possible to use an array (of any type) as function parameter. You do not have to initialize the array every time and return it back to caller function. My changes of your code as follow:

 

Utility_Rotation

 

Scriptname Utility_Rotation Hidden
; https://forums.nexusmods.com/index.php?/topic/9011983-local-to-global-rotations-that-old-chestnut/page-2

; original functions made by dafydd99


; -- FUNCTIONs -- 2 + (1)

; How to call?

; float f = Utility_Rotation.atan2(fAngleY, fAngleX)
; Utility_Rotation.zAxisRotate(objectRef, fRotationAngleZ)


;---------------------------------------
Float FUNCTION atan2(Float aY, Float aX) Global  ; public function to returning degrees
;---------------------------------------
IF ( aX )
    ; (aX != 0.0)
ELSE
    IF (aY > 0)
        RETURN  90.0    ; x=0, y>0
    ENDIF
    IF (aY < 0)
        RETURN -90.0    ; x=0, y<0
    ENDIF
        RETURN   0.0    ; x=0, y=0    undefined
ENDIF
;-------------
    float f = Math.ATAN(aY/aX)

IF (aX > 0)
    RETURN f            ; x>0, y
ENDIF
;---------
    IF (aY < 0)
        f = f - 180.0    ; x<0, y<0
    ELSE
        f = f + 180.0    ; x<0, y>=0
    ENDIF
    RETURN f            
ENDFUNCTION


;----------------------------------------------------
FUNCTION zAxisRotate(ObjectReference oRef, Float aZR) Global  ; public function
;----------------------------------------------------
    float[] a = new Float[3]           ; XYZ, aZR = angleZRotate

IF (oRef) && oRef.GetBaseObject()
    a[0] = oRef.GetAngleX()            ; originalXAngle
    a[1] = oRef.GetAngleY()            ; originalYAngle
    a[2] = oRef.GetAngleZ()            ; originalZAngle
ELSE
    Debug.Trace(" zAxisRotate() - Error: Invalid object detected!")
    RETURN ; - STOP - invalid Ref detected!
ENDIF
;--------------------
    Calculate(a, -a[2])                ; fill in normalisedAngles
    Calculate(a, (aZR - a[2]))         ; fill in angles
    oRef.SetAngle(a[0], a[1], a[2])
ENDFUNCTION


;--------------------------------------
FUNCTION Calculate(Float[] a, Float aZ) Global  ; private
;--------------------------------------
; Keep in mind: parameter "Float[] a" is a pointer to array!

    float aX = a[0]    
    float aY = a[1]
    
    aX = -aX    ; localAngX
    aY = -aY    ; localAngY
    aZ = -aZ    ; localAngZ

; store the "sine" and "cosine" of our local angles
; -------------------------------------------------
    float sx = Math.SIN(aX)
    float sy = Math.SIN(aY)
    float sz = Math.SIN(aZ)

    float cx = Math.COS(aX)
    float cy = Math.COS(aY)
    float cz = Math.COS(aZ)

; roation matrix ZYX
; ------------------
;;;    float r11 =  cz*cy
;;;    float r12 = -sz*cx + (sy*sx*cz)
;;;    float r13 =  sz*sx + (sy*cx*cz)
;;;    float r21 =  sz*cy
;;;    float r22 =  cz*cx + (sy*sx*sz)
;;;    float r23 = -sx*cz + (sy*cx*sz)
;;;    float r31 = -sy
;;;    float r32 =  cy*sx
;;;    float r33 =  cy*cx

; extract the worldangles from rotation matrix above
; --------------------------------------------------
    aY = sx*sz + (sy*cx*cz)               ; r13

    IF     (aY < -0.9998)        ; negative gimbal lock
        a[1] =  90.0
        a[2] =   0.0

        aY = sx*cy                        ; r32
        aX = cx*cz + (sx*sy*sz)           ; r22

    ELSEIF (aY > 0.9998)        ; positive gimbal lock
        a[1] = -90.0
        a[2] =   0.0

        aY = sx*cy                        ; r32
        aX = cx*cz + (sx*sy*sz)           ; r22

    ELSE                        ; no gimbal lock
        aY = Math.ASIN(aY)                ; asin(r13)
        a[1] = -aY

        aZ = -sz*cx + (sx*sy*cz)          ; r12
        aZ = atan2(-aZ, cy*cz)            ; (-r12, r11)
        a[2] = -aZ                        

        aY = -sx*cz + (cx*sy*sz)
        aY = -aY                          ;-r23
        aX = cx*cy                        ; r33
    ENDIF

        a[0] = -atan2(aY,aX)
ENDFUNCTION

 

 

Edited by ReDragon2013
Link to comment
Share on other sites

Thanks ReDragon2013 - my code had been stitched together from several other postings that I believe have already been referenced and put into a more generic form. The real breakthrough for me was a realisation that it's necessary to 'undo' the rotations (normalise) before re-applying newly calculated ones to get to the correct orientation - there must be an easier solution that doesn't require this second stage, but 3D orientation stuff is surprisingly complex, and my maths is a long way from good enough to solve it! Happy to recommend people use either your updated version or my old one depending on their code style and formatting preferences.

 

The Chesko code is very useful and I've needed it myself - but just to be clear it solves a very different problem, which is the position of a reference rotated around another reference. So, for example if you wanted a untextured sphere to rotate around a pillar, it would work fine as is. If, however you wanted to rotate a skull (specifically one which was offset in the X,Y rotations - eg to look 'down' on the player) it would also require these functions to be oriented correctly.

 

It has been frustrating that Bethesda didn't quite manage a full set of built in 3D operations - if you want an example of how incomplete they are, you can try doing something like a slow TranslateToRef to rotate an object reference at a non-zero rotation, and in certain situations it'll rotate to a totally different angle, then take a 90deg change of direction, before finally rotating to the destination position. I tried this with opening/closing a toilet lid and never found a decent resolution!

 

Cheers - dafydd99

Link to comment
Share on other sites

So to expand upon this even further, currently I'm trying to script an object to translate to another object, while always facing that object. Problem is the object I'm translating to could change position. I remembered some functions I wrote for my mod Turn Objects Up for Fallout4 that helps for this. This function will get the difference in the CenterRef's heading angle from the OrbitRef's position.

 

 

 

Static Property XMarker Auto


Float Function GetAngleOfRefFromHeading(ObjectReference akCenterRef, ObjectReference OrbitRef)
    ;Get the angle difference of the CenterRef's heading (z angle) and the orbit ref.
    ;So if the orbit ref is directly behind the center ref, it will return 180
    ;Used to make the CenterRef face the OrbitRef
    
    ObjectReference CenterRef = akCenterRef.PlaceAtMe(XMarker, 1)
    
    Float CurrentDistance = GetDistance2D(OrbitRef, CenterRef)
    Float CenterAngleZ = CenterRef.GetAngleZ()
    ObjectReference ZeroRef = CenterRef.PlaceAtMe(XMarker, 1, false, true)
    ObjectReference RightRef = CenterRef.PlaceAtMe(XMarker, 1, false, true)
    ObjectReference LeftRef = CenterRef.PlaceAtMe(XMarker, 1, false, true)
    Float XDist = Math.Sin(CenterAngleZ)
    Float YDist = math.Cos(CenterAngleZ)
    XDist *= CurrentDistance
    YDist *= CurrentDistance
    ZeroRef.MoveTo(CenterRef, XDist, YDist, OrbitRef.GetPositionZ())


    XDist = Math.Sin(CenterAngleZ + 90)
    YDist = math.Cos(CenterAngleZ + 90)
    XDist *= CurrentDistance
    YDist *= CurrentDistance
    RightRef.MoveTo(CenterRef, XDist, YDist, OrbitRef.GetPositionZ())


    XDist = Math.Sin(CenterAngleZ - 90)
    YDist = math.Cos(CenterAngleZ - 90)
    XDist *= CurrentDistance
    YDist *= CurrentDistance
    LeftRef.MoveTo(CenterRef, XDist, YDist, OrbitRef.GetPositionZ())


    If GetDistance2D(OrbitRef, RightRef) < GetDistance2D(OrbitRef, LeftRef)
        Float ADistance = GetDistance2D(CenterRef, ZeroRef)
        Float BDistance = GetDistance2D(ZeroRef, OrbitRef)
        Float CDistance = GetDistance2D(OrbitRef, CenterRef)
        Float CurrentAngleA = Math.acos( ((BDistance * BDistance) - (ADistance * ADistance) - (CDistance * CDistance)) / (2 * ADistance * CDistance) )
        Float AngleDiff = (180 - CurrentAngleA)
        CenterRef.Delete()
        ZeroRef.Delete()
        RightRef.Delete()
        LeftRef.Delete()
        Return AngleDiff
    Else
        Float ADistance = GetDistance2D(CenterRef, ZeroRef)
        Float BDistance = GetDistance2D(ZeroRef, OrbitRef)
        Float CDistance = GetDistance2D(OrbitRef, CenterRef)
        Float CurrentAngleA = Math.acos( ((BDistance * BDistance) - (ADistance * ADistance) - (CDistance * CDistance)) / (2 * ADistance * CDistance) )
        Float AngleDiff = (180 + CurrentAngleA)
        CenterRef.Delete()
        ZeroRef.Delete()
        RightRef.Delete()
        LeftRef.Delete()
        Return AngleDiff
    Endif
EndFunction


Float Function GetDistance2D(ObjectReference RefA, ObjectReference RefB)
    Float RefAX = RefA.GetPositionX()
    Float RefAY = RefA.GetPositionY()


    Float RefBX = RefB.GetPositionX()
    Float RefBY = RefB.GetPositionY()


    Float Distance = Math.sqrt( ((RefAX-RefBX) * (RefAX-RefBX)) + ((RefAY-RefBY) * (RefAY-RefBY)) )
    Return Distance
EndFunction

I tested it with this function to make the player face an object.
Float AngleDiff = GetAngleOfRefFromHeading(PlayerRef, ConsoleUtil.GetSelectedReference())
PlayerRef.SetAngle(PlayerRef.GetAngleZ(), PlayerRef.GetAngleY(), PlayerRef.GetAngleZ() + AngleDiff)

It worked like a charm. No matter where I was around the object, or which direction I was facing, it would make me face the object. And if you need to keep relative X and Y angles, you can use your function

zAxisRotate(PlayerRef, PlayerRef.GetAngleZ() + AngleDiff)
Link to comment
Share on other sites

I've been watching this threat to observe the different point of views cause its quite interesting.

Although all this can be done easier if you do it on the nif level, but that's another thing...


@ AnishaDawn

This is not a "Smart's" competition and all opinions and point of views are welcome from every individuals modding level.

You don't need to be smart enough or the smarter of all to fit in here, the only required thing to fit in is to have the unconditional desire to help a fellow modder by sharing knowledge, knowhow and occationaly creating something for them to help them make their ideas into reality.


* The only way to create better future modders and push forwards the gaming industry is to willingly educate and learn from each other.


* Like it or not modders have shaped the gaming scene as it is today, every single innovation and leap forwards was done by modders which the industry took advantage and in some cases even stole.

Edited by maxarturo
Link to comment
Share on other sites

@dylbill - looks good! Glad you're finding more usages for this. One tiny typo might be in calling this function - PlayerRef.SetAngle(PlayerRef.GetAngleZ().... - that should probably be GetAngleX()?

 

@AnishaDawn - I very much agree with Maxaturo here - there's so much that can be done with a mod, being good at everything is next to impossible. Even the experts who worked on Skyrim would likely not be able to do everything. And the area we're looking at here, 3D maths is hard - super hard. Like maths degree level hard. 2D maths can be tricky, but 3D is another level beyond, and as such you'd usually have all the stuff needed built into the scripting language libraries - but even Bethesda couldn't manage that completely. It took months for me to get close to something that works from various examples I found online and a lot of trial and error and I still don't really understand it! Just glad it works - lol!

 

@maxaturo - re nifs, yes you're quite right, but it does depend on the application, and in some 'attach ref' might do the trick. This is hopefully the 'general' scripting solution for the problem, and as such can help in situations where creating a new nif everytime might not be ideal. Eg as a modder I'd prefer to be able to reposition/rotate references in the creation kit than change the nif that represents them.

Link to comment
Share on other sites

@dafydd99 Yes, nice catch. That was a type, it should be GetAngleX()

 

@AnishaDawn, I agree with Maxaturo and Dafdd99 as well. The functions I posted I had to google a lot to learn how to do, then I also had to do a bunch of trial and error to make them work in Skyrim. As Maxaturo said, all view points are welcome and will help modders make what they want to make.

 

@maxaturo, agree most of the time setting things up directly in the nif is easier, but as dafydd99 said, it depends on the application. For my case, translating and object to a moving object and keeping it facing the object can't really be done in a nif, so it's good to know these papyrus functions when using only a nif isn't possible.

Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...