Search Unity

Question How to rotate an entity but lock a specific axis?

Discussion in 'Entity Component System' started by Waka-Takaki, Oct 5, 2021.

  1. Waka-Takaki

    Waka-Takaki

    Joined:
    Jan 19, 2014
    Posts:
    27
    Hi, I'm creating a practice project to learn how to use the ECS system. I have an inverted sphere (flipped normals) which players (currently capsules) can run around the inside of. I use Unity Physics (but this question is not physics related). I have a gravity system I have implemented which pushes players to the sphere walls (away from the sphere center).

    I am trying to implement a system that keeps the player's "up" direction, pointed at the sphere center, so when they travel forward they naturally move around the inside of the sphere in an orbit. It rotates the capsules fine, but I need it to not rotate the y-axis, just the x and z, so that the player continues running in the direction they want to travel in. Here is what I have so far:

    Code (CSharp):
    1. [UpdateAfter(typeof(TransformSystemGroup))]
    2. public class RotateUpToSphereCenterSystem : SystemBase
    3. {
    4.     protected override void OnUpdate()
    5.     {
    6.         float deltaTime = Time.DeltaTime;
    7.  
    8.         Entities
    9.             .ForEach((ref Rotation rotation, ref MovementData movement, in LocalToWorld ltw, in Translation translation) =>
    10.             {
    11.                 Translation sphereCenter = GetComponent<Translation>(movement.sphere);
    12.  
    13.                 float3 dif = ltw.Up - ltw.Forward;
    14.                 float3 newUp = math.normalize(sphereCenter.Value - translation.Value);
    15.                 float3 newForward = math.normalize(newUp - dif);
    16.  
    17.                 // I need the newRotation to have the same y rotation as the original rotation
    18.                 quaternion newRotation = quaternion.LookRotation(newForward, newUp);
    19.                
    20.                 rotation.Value = newRotation;
    21.             }).ScheduleParallel();
    22.     }
    23. }
    I'm struggling to wrap my head around this when we don't have a quaternion.toEuler(). Any help would be much appreciated.
     
  2. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Could try storing player input rotation separately as an angle.
    Then use quaternion.AxisAngle(newUp, *angle*) to create rotation based on the new up axis and stored angle. (Just in case - AxisAngle takes radians, not degrees)

    Finally combine newRotation with Y-axis rotation (use math.mul(newRotation, *axisAngleRotation*));
     
  3. MaNaRz

    MaNaRz

    Joined:
    Aug 24, 2017
    Posts:
    117
    Checking the Docs of the Transform Package i see that there is a RotationEulerXYZ IComponentData which is then applied in an "RotationEulerSystem". If you look inside of that system you should find everything you need. They (Unity) write to the actual Rotation Component like this :

    Code (CSharp):
    1.  
    2. chunkRotations[i] = new Rotation                    
    3. {                      
    4.     Value = quaternion.EulerXYZ(chunkRotationEulerXYZs[i].Value)            
    5. };
     
  4. Waka-Takaki

    Waka-Takaki

    Joined:
    Jan 19, 2014
    Posts:
    27
  5. Waka-Takaki

    Waka-Takaki

    Joined:
    Jan 19, 2014
    Posts:
    27
    quaternion.AxisAngle() doesn't seem to help, because I can't work out how to retrieve any rotation values as angles. The player's current forward is just from LocalToWorld.Forward. So, I just have a float3 vector. And there doesn't seem to be a function to extract the Euler angles from the quaternion rotation.

    The problem I'm having here is I don't have any Euler angles to begin with. I have only have the entity quaternion rotation and LocalToWorld. So, although I can create a new quaternion from XYZ coords using quaternion.Euler() I don't know what values to input, because all my input rotation data is in quaternion form.

    Let me use a simplified example:

    Code (CSharp):
    1. // Imagine my entity has this rotation (I don't know any of the angles though)
    2. quaternion rot = math.Euler(math.radians(45), math.radians(45), math.radians(45));
    3.  
    4. // Now if I want to zero out the Y axis in this rotation, for example, how would I do this?
    5.  
     
  6. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    Well, you're converting input to the forward vector at some point, right?
    Could just store that value and simplify things by a lot.
    (e.g. in case you're going to modify rotation in multiple data processing steps / systems);


    Alternatively, you can get angle between two vectors (forward and X-axis - by taking a cross product of forward and custom Y axis). Although I'm not too sure about order of vectors, might want to try out different / inverse one based on the result angle.

    In any case, if you're 100% sure you want to work on eulers instead, here's a thread with a bunch of useful methods how to convert quaternion to float3 euler representation. (there's even one that I made back in 2019)
    https://forum.unity.com/threads/is-there-a-conversion-method-from-quaternion-to-euler.624007/
     
    Last edited: Oct 6, 2021
  7. Waka-Takaki

    Waka-Takaki

    Joined:
    Jan 19, 2014
    Posts:
    27
    Thanks for your help Vergil. I struggled with this for another evening and posted a question on the gamedev stack site. I gained a better understanding of the issue by following some posts in an answer. (https://gamedev.stackexchange.com/q...ternion-to-point-in-a-direction/197219#197219)

    Basically, quaternion.LookRotation(forward, up) sets the forward to be exactly the forward direction provided, and the up is set as close as possible afterwards. This is what you want in most cases. But in some cases (such as this one) you want up to be exact and forward to be as close as possible. So, I've implemented an extension method to provide this functionality.

    Code (CSharp):
    1. public static class mathx
    2. {
    3.     public static quaternion LookRotationExactUp(float3 approximateForward, float3 exactUp)
    4.     {
    5.         quaternion rotateZToUp = quaternion.LookRotation(exactUp, -approximateForward);
    6.         quaternion rotateYToZ = quaternion.RotateX(math.radians(90));
    7.  
    8.         return math.mul(rotateZToUp, rotateYToZ);
    9.     }
    10. }
    And the implementation:

    Code (CSharp):
    1. [UpdateAfter(typeof(TransformSystemGroup))]
    2. public class RotateUpToSphereCenterSystem : SystemBase
    3. {
    4.     protected override void OnUpdate()
    5.     {
    6.         float3 sphereCenter = new float3(0, 0, 0);
    7.         float deltaTime = Time.DeltaTime;
    8.         float rotationSpeed = 5;
    9.  
    10.         Entities
    11.             .ForEach((ref Rotation rotation, ref MovementData movement, in LocalToWorld ltw) =>
    12.             {
    13.                 // Only rotate if not at exact center of sphere
    14.                 if (!ltw.Position.Equals(sphereCenter))
    15.                 {
    16.                     float3 centerDir = math.normalize(sphereCenter - ltw.Position);
    17.  
    18.                     float3 newUp = centerDir;
    19.  
    20.                     // Guarantee exact up direction and maintain forward as close as possible
    21.                     movement.targetRotation = mathx.LookRotationExactUp(ltw.Forward, newUp);
    22.  
    23.                     // Smoothly rotate to the targetRotation
    24.                     rotation.Value = math.slerp(rotation.Value, movement.targetRotation, deltaTime * rotationSpeed);
    25.                 }
    26.             }).ScheduleParallel();
    27.     }
    28. }
     
    Last edited: Oct 9, 2021
    Occuros and xVergilx like this.
  8. unity_9GTK35cY9R08zQ

    unity_9GTK35cY9R08zQ

    Joined:
    Sep 9, 2020
    Posts:
    10
    this was insanely helpful, thank you. now to just wrap my head around exactly why it works