rotation glitches when reaching 360 degrees

DJMDJM Posts: 35Member
edited November 26 in General Support

im trying to create weapon sway based on the cameras' and players' rotation, the main idea works but when reaching the 360degrees it glitches. maybe i need the local rotation , but i cant figure it out.
how do i make it work? tnx

func doweaponsway(delta):
    #calculate rotation



    var wantedz
    var wantedx
    var wantedy


    var xrot = $CamRoot.rotation_degrees.x  - oldxrot
    var yrot = self.rotation_degrees.y- oldyrot
    wantedx = lerp(weaponsway.rotation_degrees.x,xrot,2 * delta)
    wantedy = lerp(weaponsway.rotation_degrees.y,yrot,2 * delta)


    weaponsway.rotation_degrees.x = clamp(wantedx, -2, 2)
    weaponsway.rotation_degrees.y = clamp(wantedy, -2, 2)
    #weaponsway.rotation_degrees.z =clamp(wantedz,-2,2)

    oldxrot = $CamRoot.rotation_degrees.x 
    oldyrot = self.rotation_degrees.y
«1

Comments

  • TwistedTwiglegTwistedTwigleg Posts: 5,028Admin
    edited November 26

    Welcome to the forums @DJM!

    I would recommend having the rotation for each axis in the weapon sway be a different node. This is because in Godot (and other game engines), having multiple axes of rotation that go over 180 degrees can lead to other axes being flipped to negative 180. I have found that when working with rotation and clamping, using separate nodes for each axes of rotation makes it MUCH easier. For example, instead of having weaponsway modified on the X and Y, you'd want to have something like this:

    • weaponsway_x
      • weaponsway_y
        • (other weapon sway nodes here)

    Then in your code you can modify the rotation_degrees of weaponsway_x for the x axis and weaponsway_y for the y axis. That may help with the 360 degrees issue if the reason it stops working is because of the -180 degree issue.

    Also, using rotation_degrees is the local rotation. If you want the global rotation of a node, you'll need to use node.global_transform.basis to get the node's global rotations :smile:

  • DJMDJM Posts: 35Member

    tnx! im happy ive made the switch to godot.
    sadly seperating the rotation axis on different nodes doesnt fix my issue.
    ive also tried the global transform basis u suggested

    in unity i used to use >
    Mathf.DeltaAngle to get the shortest distance between rotations, i dont know if there is a similar function in gdscript

  • TwistedTwiglegTwistedTwigleg Posts: 5,028Admin

    @DJM said:
    tnx! im happy ive made the switch to godot.
    sadly seperating the rotation axis on different nodes doesnt fix my issue.
    ive also tried the global transform basis u suggested

    A shame that didn't fix it, but thankfully it means it's not the -180 issue that is causing the problem! I've found the -180 problem can be tricky to work around, so that's good that it's not what is causing the issue here.

    in unity i used to use >
    Mathf.DeltaAngle to get the shortest distance between rotations, i dont know if there is a similar function in gdscript

    I don't think there is anything built in that is similar to the Mathf.DeltaAngle function unfortunately.
    This StackOverflow solution provides a language agnostic way to get the difference of angles, though I'm not sure if the mod function in GDScript returns the sign or not. This Godot StackOverflow question also has an answer with many examples on lerping between two angles, which may work for getting the difference as well.

  • DJMDJM Posts: 35Member

    actually i dont want to lerp between two angles. i only need to get the float between the current and last rotation per frame to do weaponsway calculations.
    the issue thats occurring whit my current code is that when the current rotation is 0 and the last was 359 the weapon sway gets a short glitch because of the huge difference when i subtract the values

  • cyberealitycybereality Posts: 2,685Moderator
    edited November 26

    This is a problem with Euler angles. As they go from 0 to 360 (or -180 to 180) then you will always have problems at the end of the range, especially when interpolating the value. Think of it like this, you want to rotate 30 degrees. But the angle is from 340 to 370 (or 10). If you just interpolate the values (340 and 10) instead of rotating 30 degrees, it will rotate 330 degrees, and in the wrong direction. You can account of this by looking at the values and adjusting them, but it is an inherent issue with using Euler angles.

    A few things can help. First, try to always get 0 to 360 to make the math easier (so you don't get negative values or values above 360):

    var angle = fmod(angle + 360.0, 360.0)
    

    Then you can find the smaller angle. If you know your delta angle will never be more than 180 degrees (a pretty fair assumption for most games):

    var angle_delta = angle - last_angle
    angle_delta = fmod(angle_delta + 360.0, 360.0)
    if angle_delta > 180.0:
        angle_delta = angle_delta - 360.0
    
  • xyzxyz Posts: 451Member
    edited November 26

    Working with separate (euler) angles in 3d is just not worth the effort. You're in for a constant barrage of annoying gotchas :)
    Do it the grownup way and represent your current and wanted orientation with quaternions, and interpolate between them. You can even simplify by just using two lookat vectors and slerp between them.

    var current: Vector3 # current lookat direction
    var wanted: Vector3 # wanted lookat direction
    
    func _process(delta):
        wanted = calculate_wanted()
        current = current.slerp(wanted, fraction)
        look_at(current, Vector3.UP)
    
  • DJMDJM Posts: 35Member

    tnx ! that works perfect

    heres the code in case anybody wonders to do the same thing

    `func doweaponsway(delta):
    #calculate rotation

    var xrot = fmod( $CamRoot.rotation_degrees.x + 360.0, 360)
    var xrotdelta = xrot - oldxrot
    xrotdelta = fmod(xrotdelta + 360.0, 360.0)
    if xrotdelta > 180:
        xrotdelta = xrotdelta - 360
    
    var yrot = fmod( self.rotation_degrees.y + 360.0, 360)
    var yrotdelta = yrot - oldyrot
    yrotdelta = fmod(yrotdelta + 360.0, 360.0)
    if yrotdelta > 180:
        yrotdelta = yrotdelta - 360
    
    
    
    var wantedx = lerp(weaponsway.rotation_degrees.x,xrotdelta,2 * delta)
    var wantedy = lerp(weaponsway.rotation_degrees.y,yrotdelta,2 * delta)
    
    
    
    weaponsway.rotation_degrees.x = clamp(wantedx, -2, 2)
    weaponsway.rotation_degrees.y = clamp(wantedy, -2, 2)
    
    
    
    
    oldxrot =fmod( $CamRoot.rotation_degrees.x + 360.0, 360)
    oldyrot = fmod( self.rotation_degrees.y + 360.0, 360)
    

    `

  • DJMDJM Posts: 35Member

    @xyz said:
    Working with separate (euler) angles in 3d is just not worth the effort. You're in for a constant barrage of annoying gotchas :)
    Do it the grownup way and represent your current and wanted orientation with quaternions, and interpolate between them. You can even simplify by just using two lookat vectors and slerp between them.

    var current: Vector3 # current lookat direction
    var wanted: Vector3 # wanted lookat direction
    
    func _process(delta):
      wanted = calculate_wanted()
      current = current.slerp(wanted, fraction)
      look_at(current, Vector3.UP)
    

    im not sure how to use that,
    how to calculate "wanted" and what is "fraction" in your example?

  • cyberealitycybereality Posts: 2,685Moderator

    Wanted would be the next angle, current would be the previous angle, and fraction is a variable you set that controls how fast it interpolates.

  • cyberealitycybereality Posts: 2,685Moderator
    edited November 26

    And yes, avoid using Euler angles if you can. They make things easier in the beginning, and they are simpler to understand, but using more advanced structures like Quaternions end up with more robust functionality, less bugs, and less code.

  • DJMDJM Posts: 35Member

    @cybereality said:
    And yes, avoid using Euler angles if you can. They make things easier in the beginning, and they are simpler to understand, but using more advanced structures like Quaternions end up with more robust functionality, less bugs, and less code.

    ok
    im having trouble figuring out the local forward lookat from the camroot.
    if i try the suggestion the weapon doesnt seem to point into the view direction im looking

  • xyzxyz Posts: 451Member
    edited November 26

    @DJM said:

    @xyz said:
    Working with separate (euler) angles in 3d is just not worth the effort. You're in for a constant barrage of annoying gotchas :)
    Do it the grownup way and represent your current and wanted orientation with quaternions, and interpolate between them. You can even simplify by just using two lookat vectors and slerp between them.

    var current: Vector3 # current lookat direction
    var wanted: Vector3 # wanted lookat direction
    
    func _process(delta):
        wanted = calculate_wanted()
        current = current.slerp(wanted, fraction)
        look_at(current, Vector3.UP)
    

    im not sure how to use that,
    how to calculate "wanted" and what is "fraction" in your example?

    This was more at pseudocode level just to show the principle. We can discuss specifics but it'd be much easier if you could post how your nodes are set up.

  • DJMDJM Posts: 35Member

    @xyz said:

    @DJM said:

    @xyz said:
    Working with separate (euler) angles in 3d is just not worth the effort. You're in for a constant barrage of annoying gotchas :)
    Do it the grownup way and represent your current and wanted orientation with quaternions, and interpolate between them. You can even simplify by just using two lookat vectors and slerp between them.

    var current: Vector3 # current lookat direction
    var wanted: Vector3 # wanted lookat direction
    
    func _process(delta):
      wanted = calculate_wanted()
      current = current.slerp(wanted, fraction)
      look_at(current, Vector3.UP)
    

    im not sure how to use that,
    how to calculate "wanted" and what is "fraction" in your example?

    This was more at pseudocode level just to show the principle. We can discuss specifics but it'd me much easier if you could post how your nodes are set up.

    here u go

  • xyzxyz Posts: 451Member
    edited November 26

    Ok, here's a quick version using quaternions:

    func _process(delta):
        var softness = 3.0
        do_sway($weaponsway, calculate_sway_offset(), delta * softness)
    
    func do_sway(sway_node, offset: Vector2, fract: float):
        var wanted_quat = Quat(Vector3.UP, offset.x) * Quat(Vector3.RIGHT, offset.y)
        var current_quat = Quat(sway_node.transform.basis)
        sway_node.transform.basis = Basis(current_quat.slerp(wanted_quat, fract))
    

    You just need to implement calculate_sway_offset() to return horizontal and vertical sway offsets as Vector2. You can calculate it as you currently do from camera rotation deltas between frames.

  • DJMDJM Posts: 35Member

    @xyz said:
    Ok, here's a quick version using quaternions:

    func _process(delta):
      var softness = 3.0
      do_sway($weaponsway, calculate_sway_offset(), delta * softness)
    
    func do_sway(sway_node, offset: Vector2, fract: float):
      var wanted_quat = Quat(Vector3.UP, offset.x) * Quat(Vector3.RIGHT, offset.y)
      var current_quat = Quat(sway_node.transform.basis)
      sway_node.transform.basis = Basis(current_quat.slerp(wanted_quat, fract))
    

    You just need to implement calculate_sway_offset() to return horizontal and vertical sway offsets as Vector2. You can calculate it as you currently do from camera rotation deltas between frames.

    what do i put in the "calculate sway offset" func? how to return the vector2 values?

    im currently calculating the movement in degrees so wont that have the same euler issues as i allready had?

  • xyzxyz Posts: 451Member
    edited November 27

    I'd do it from controller/mouse deltas, or if you must deal with movement - again from transformation basis/quaternions.

  • xyzxyz Posts: 451Member
    edited November 27

    Here's a way to calculate offsets from difference in rotation between frames. Using quaternions will ensure there are no wrapping and gimbal problems. You'll need to maintain player/camera basis from the previous frame:

    calculate_sway_offset(basis_last_frame: Basis, basis_current_frame: Basis):
        var q1 = basis_last_frame.get_rotation_quat()
        var q2 = basis_current_frame.get_rotation_quat()
        var q = q1.inverse() * q2
        var eu = q.get_euler()
        return Vector2(eu.y, eu.x)
    
  • cyberealitycybereality Posts: 2,685Moderator

    Right, but this is also why Euler is much easier when starting out. It took me years to fully understand Quaternions, but I guess if you use Godot it does most of the math for you (but you still kind of have to understand what you're doing).

  • DJMDJM Posts: 35Member
    edited November 27

    @xyz said:
    Here's a way to calculate offsets from difference in rotation between frames. Using quaternions will ensure there are no wrapping and gimbal problems. You'll need to maintain player/camera basis from the previous frame:

    calculate_sway_offset(basis_last_frame: Basis, basis_current_frame: Basis):
      var q1 = basis_last_frame.get_rotation_quat()
      var q2 = basis_current_frame.get_rotation_quat()
      var q = q1.inverse() * q2
      var eu = q.get_euler()
      return Vector2(eu.y, eu.x)
    

    ok heres what ive got right now, based on your code

    func _process(delta):
    
        var softness = 3.0
        do_sway(weaponsway, calculate_sway_offset(), delta * softness)
    
    
    calculate_sway_offset(basis_last_frame: Basis, basis_current_frame: Basis):
        var q1 = basis_last_frame.get_rotation_quat()
        var q2 = basis_current_frame.get_rotation_quat()
        var q = q1.inverse() * q2
        var eu = q.get_euler()
        return Vector2(eu.y, eu.x)
    
    
    func do_sway(sway_node, offset: Vector2, fract: float):
        var offset_quaternion: Quat = Quat(Vector3.UP, offset.x) * Quat(Vector3.RIGHT, offset.y)
        sway_node.transform.basis = Basis(Quat(sway_node.transform.basis).slerp(offset_quaternion, fract))
    

    im not sure what to write in the > do_sway(weaponsway, calculate_sway_offset( "what goes here?"), delta * softness)
    i cant use mouse rotation , because it will sway the weapon even if theres no rotation happening.

  • xyzxyz Posts: 451Member
    edited November 27

    @DJM said:
    im not sure what to write in the > do_sway(weaponsway, calculate_sway_offset( "what goes here?"), delta * softness)
    i cant use mouse rotation , because it will sway the weapon even if theres no rotation happening.

    What were you trying to base the sway on in the first place? It's typically done based on mouse input. In your initial post you said you want to base it on player and camera rotations. I'm not sure I understand why are you trying to base it on both.

    In my example, I assumed you want to do it based on changes in camera rotation - bigger the change, larger the offset. To do this you use camera rotations in previous and current frames (represented by transform basis) as arguments to that function. The function extracts "horizontal" and "vertical" camera rotatation deltas from total rotation of the camera between consecutive frames.

    var basis_last_frame = Basis()
    func _process(delta):
        var basis_this_frame = $cam.transform.basis
        var offset = calculate_sway_offset(basis_last_frame, basis_this_frame)
        do_sway($sway, offset, delta * softness)
        basis_last_frame = basis_this_frame;
    

    $sway and $cam are paths to your actual sway and camera nodes. In fact $cam stands for the node that does actual rotation in respect to global space. I'm assuming it's the player node in your case.

    Of course you can base this offset on other things, depending on your system and actual effect you want to achieve. So it's up to you to decide that. I'd base it simply on InputEventMouseMotion.relative. In which case the whole calculate_sway_offset() function in my previous post can simply return mouse offsets. It just needs to return zero offset if mouse motion didn't happen in the current frame and it'll all work fine.

  • xyzxyz Posts: 451Member
    edited November 27

    @cybereality said:
    Right, but this is also why Euler is much easier when starting out. It took me years to fully understand Quaternions, but I guess if you use Godot it does most of the math for you (but you still kind of have to understand what you're doing).

    One doesn't really need to fully understand quaternions and all the related math to be able to use them for 3d graphics. They have a wide general usage in math but in the world of computer graphics quaternion can be seen simply as a matrix-light that covers only rotations. You can use it as a black box that has certain neat properties and does certain useful things for you. Most people successfully use 4x4 transformation matrices this way.

    But yeah, it's true that quaternions may look like an overkill for simple stuff that can kinda be solved using eulers. Especially when you see them for the first time. However eulers grossly fall short if you need to deal with lots of 3d orientation/targeting stuff, as is the case with first person shooters. Quaternions can handle most of such problems with ease in just a few lines of code.

  • DJMDJM Posts: 35Member

    @xyz said:

    @DJM said:
    im not sure what to write in the > do_sway(weaponsway, calculate_sway_offset( "what goes here?"), delta * softness)
    i cant use mouse rotation , because it will sway the weapon even if theres no rotation happening.

    What were you trying to base the sway on in the first place? It's typically done based on mouse input. In your initial post you said you want to base it on player and camera rotations. I'm not sure I understand why are you trying to base it on both.

    In my example, I assumed you want to do it based on changes in camera rotation - bigger the change, larger the offset. To do this you use camera rotations in previous and current frames (represented by transform basis) as arguments to that function. The function extracts "horizontal" and "vertical" camera rotatation deltas from total rotation of the camera between consecutive frames.

    var basis_last_frame = Basis()
    func _process(delta):
      var basis_this_frame = $cam.transform.basis
      var offset = calculate_sway_offset(basis_last_frame, basis_this_frame)
      do_sway($sway, offset, delta * softness)
      basis_last_frame = basis_this_frame;
    

    $sway and $cam are paths to your actual sway and camera nodes. In fact $cam stands for the node that does actual rotation in respect to global space. I'm assuming it's the player node in your case.

    Of course you can base this offset on other things, depending on your system and actual effect you want to achieve. So it's up to you to decide that. I'd base it simply on InputEventMouseMotion.relative. In which case the whole calculate_sway_offset() function in my previous post can simply return mouse offsets. It just needs to return zero offset if mouse motion didn't happen in the current frame and it'll all work fine.

    tnx for your reply
    it sort of works but sway happens only on one axis, if i set $cam to the player node it rotates horizontaly if i set $cam to the camera node it happens only vertically

    idont ant to use mouse motion as when u are looking down and the rotation is clamped , u will still get weapon sway even if there is no actual rotation happening

  • xyzxyz Posts: 451Member
    edited November 27

    Well that's probably because different nodes handle rotations on different axes in your setup.
    You can simply do the basis thing for both nodes, calculate both offsets and then use horizontal offset from one, and vertical offset from the other.

    var cam_basis_last_frame = Basis()
    var player_basis_last_frame = Basis()
    
    func _process(delta):
        var cam_basis_this_frame = $cam.transform.basis 
        var player_basis_this_frame = $player.transform.basis
        var offset1 = calculate_sway_offset(cam_basis_last_frame, cam_basis_this_frame)
        var offset2 = calculate_sway_offset(player_basis_last_frame, player_basis_this_frame)
        var offset = Vector2(offset1.x, offset2.y) #may need to swap 1 and 2
        do_sway($sway, offset, delta * softness)
        cam_basis_last_frame = cam_basis_this_frame;
        player_basis_last_frame = player_basis_this_frame;
    
  • xyzxyz Posts: 451Member
    edited November 27

    I'm beginning to think that wrapped-euler approach may be simpler for calculating the offsets here :)

  • DJMDJM Posts: 35Member

    @xyz said:
    I'm beginning to think that wrapped-euler approach may be simpler for calculating the offsets here :)

    so my initial idea was better?

  • xyzxyz Posts: 451Member
    edited November 27

    @DJM said:

    @xyz said:
    I'm beginning to think that wrapped-euler approach may be simpler for calculating the offsets here :)

    so my initial idea was better?

    It's the same thing. Quaternions will elegantly eliminate all wrapping and gimbal lock glitches, which was the problem in the first place. Since angles are already separated into different nodes, @cybereality's euler wrap solution will do that job as well. However, even if offset is obtained directly from euler angles I'd still use quaternion slerping for setting the sway node rotation.

    If you're working on a first person shooter thingy, don't shy sway from quaternions, but rather get acquainted with them. You'll need their help again... sooner or later :)

  • DJMDJM Posts: 35Member

    @xyz said:
    Well that's probably because different nodes handle rotations on different axes in your setup.
    You can simply do the basis thing for both nodes, calculate both offsets and then use horizontal offset from one, and vertical offset from the other.

    var cam_basis_last_frame = Basis()
    var player_basis_last_frame = Basis()
    
    func _process(delta):
      var cam_basis_this_frame = $cam.transform.basis 
      var player_basis_this_frame = $player.transform.basis
      var offset1 = calculate_sway_offset(cam_basis_last_frame, cam_basis_this_frame)
      var offset2 = calculate_sway_offset(player_basis_last_frame, player_basis_this_frame)
      var offset = Vector2(offset1.x, offset2.y) #may need to swap 1 and 2
      do_sway($sway, offset, delta * softness)
      cam_basis_last_frame = cam_basis_this_frame;
      player_basis_last_frame = player_basis_this_frame;
    

    tried it, now the gunsway rotates out of camera view and just gets all the rotation from the player and cam applied

  • xyzxyz Posts: 451Member

    Let's see the actual code.

  • DJMDJM Posts: 35Member

    @xyz said:
    Let's see the actual code.

    `func _process(delta):
    window_activity()
    var softness = 3.0
    var cam_basis_last_frame = Basis()
    var player_basis_last_frame = Basis()

    var cam_basis_this_frame = $CamRoot.transform.basis 
    var player_basis_this_frame = self.transform.basis
    var offset1 = calculate_sway_offset(cam_basis_last_frame, cam_basis_this_frame)
    var offset2 = calculate_sway_offset(player_basis_last_frame, player_basis_this_frame)
    var offset = Vector2(offset2.x, offset1.y) #may need to swap 1 and 2
    do_sway(weaponsway, offset, delta * softness)
    cam_basis_last_frame = cam_basis_this_frame;
    player_basis_last_frame = player_basis_this_frame;
    

    func calculate_sway_offset(basis_last_frame: Basis, basis_current_frame: Basis):
    var q1 = basis_last_frame.get_rotation_quat()
    var q2 = basis_current_frame.get_rotation_quat()
    var q = q1.inverse() * q2
    var eu = q.get_euler()
    return Vector2(eu.y, eu.x)

    func do_sway(sway_node, offset: Vector2, fract: float):
    var offset_quaternion: Quat = Quat(Vector3.UP, offset.x) * Quat(Vector3.RIGHT, offset.y)
    sway_node.transform.basis = Basis(Quat(sway_node.transform.basis).slerp(offset_quaternion, fract))
    `

  • xyzxyz Posts: 451Member
    edited November 27

    cam_basis_last_frame and player_basis_last_frame need to be sctipt-level properties, not local variables. They are used to "remember" basis values from the last frame. If you declare them as local variables inside _process(), like you're doing now, they'll just be deleted when function exits, serving no purpose.

    p.s. please fix the code formatting.

Leave a Comment

BoldItalicStrikethroughOrdered listUnordered list
Emoji
Image
Align leftAlign centerAlign rightToggle HTML viewToggle full pageToggle lights
Drop image/file