How to rotate an entity but lock a specific axis?

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:

[UpdateAfter(typeof(TransformSystemGroup))]
public class RotateUpToSphereCenterSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;

        Entities
            .ForEach((ref Rotation rotation, ref MovementData movement, in LocalToWorld ltw, in Translation translation) =>
            {
                Translation sphereCenter = GetComponent<Translation>(movement.sphere);

                float3 dif = ltw.Up - ltw.Forward;
                float3 newUp = math.normalize(sphereCenter.Value - translation.Value);
                float3 newForward = math.normalize(newUp - dif);

                // I need the newRotation to have the same y rotation as the original rotation
                quaternion newRotation = quaternion.LookRotation(newForward, newUp);
                
                rotation.Value = newRotation;
            }).ScheduleParallel();
    }
}

I’m struggling to wrap my head around this when we don’t have a quaternion.toEuler(). Any help would be much appreciated.

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));

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 :

chunkRotations[i] = new Rotation                     
{                       
    Value = quaternion.EulerXYZ(chunkRotationEulerXYZs[i].Value)            
};

Thanks for your help both of you. I can’t seem to locate any documentation and am flying blind.
Where is the sourcecode with xml summary? I see multiple posts listing extensive summaries but I can’t find them.
I looked here for docs, but they are just stubs:
https://docs.unity3d.com/Packages/com.unity.entities@0.17/api/Unity.Transforms.RotationEulerXYZ.html

(I will open another post about this so it’s easier to find in future)

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:

// Imagine my entity has this rotation (I don't know any of the angles though)
quaternion rot = math.Euler(math.radians(45), math.radians(45), math.radians(45));

// Now if I want to zero out the Y axis in this rotation, for example, how would I do this?

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://discussions.unity.com/t/731052

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. (unity - Rotating only 2 axes of rotation quaternion to "point" in a direction - Game Development Stack Exchange)

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.

public static class mathx
{
    public static quaternion LookRotationExactUp(float3 approximateForward, float3 exactUp)
    {
        quaternion rotateZToUp = quaternion.LookRotation(exactUp, -approximateForward);
        quaternion rotateYToZ = quaternion.RotateX(math.radians(90));

        return math.mul(rotateZToUp, rotateYToZ);
    }
}

And the implementation:

[UpdateAfter(typeof(TransformSystemGroup))]
public class RotateUpToSphereCenterSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float3 sphereCenter = new float3(0, 0, 0);
        float deltaTime = Time.DeltaTime;
        float rotationSpeed = 5;

        Entities
            .ForEach((ref Rotation rotation, ref MovementData movement, in LocalToWorld ltw) =>
            {
                // Only rotate if not at exact center of sphere
                if (!ltw.Position.Equals(sphereCenter))
                {
                    float3 centerDir = math.normalize(sphereCenter - ltw.Position);

                    float3 newUp = centerDir;

                    // Guarantee exact up direction and maintain forward as close as possible
                    movement.targetRotation = mathx.LookRotationExactUp(ltw.Forward, newUp);

                    // Smoothly rotate to the targetRotation
                    rotation.Value = math.slerp(rotation.Value, movement.targetRotation, deltaTime * rotationSpeed);
                }
            }).ScheduleParallel();
    }
}

this was insanely helpful, thank you. now to just wrap my head around exactly why it works