Rotation of sphere precisely aligned to mouse position

I am just starting with Flax Engine so please forgive my stupid, novice questions.
I’m also just using it for fun so I’m really lacking in proper game dev knowledge.

I’m trying to set up a spherical object (eventually a hex sphere) in the middle of the space and a camera just to the side of it. The camera should be able to zoom in an out, so sometimes the full sphere would be in view, but other times there would be just a portion of it.

I’m trying to make it so that I rotate the sphere by grabbing it with a mouse click and moving the mouse around. I’ve played around with raycasting and mouse events, quaternions, directions, rotations and all the properties of the sphere actor and I cannot get it right.

The goal is that the rotation would start on mouse click and then, as I move the mouse, the point on the sphere where the ray initially hit, would keep being under the mouse cursor until I release it.


My problem is that the tutorial cameras normally rotate the camera based on some mouse speed variable, so the object being rotated is not in perfect sync with the mouse,

Also I do not understand which of the Direction, Rotation or Orientation properties I should use.

Any idea how to achieve this? I’ve looked through the tutorials, samples and forums, but I cannot find something close to this.

Previous response A and B…?

Thank you very much for the reply.

This is quite close to some things that I’ve tried so I’m going to look through your code and comments one by one and see how this approach differs from what I did.

Even though this is just for fun, I’m trying to learn a bit as well, so I’m not looking just for code to copy paste.

That being said, I am very confused that so much of the code does not compile.

I’m using the latest Flax 1.8 and the I’m looking in the documentation as well, but

  • neither Script nor Actor have GetComponent<>
  • Script does not define OnMouseDown and OnMouseUp to override
  • Input.GetMouseRay does not exist
  • neither is Physics.Raycast
  • or Input.GetMouseWorldPosition

Am I using the wrong thig? Are there extensions that I should install, or am I on a completely different version of the API?

1 Like

I don’t want to speculate, but I think you found out the evidences of an AI generated answer (Unity based).

1 Like

I think it is, which puts me in the exact position that I was in initially.

I’ve played a bit with Unity and Stride before, so I’m looking to get some answer specific to Flax because the devil seems to be in the details.

I’ve got something almost building here, but I cannot run it so I don’t know how far off I am:

    public override void OnUpdate() {
        // Here you can add code that needs to be called every frame
        var pos = Input.MousePosition;
        var ray = Camera.MainCamera.ConvertMouseToRay( pos );

        if( Physics.RayCast( ray.Position, ray.Direction, out var hit ) ) {
            if( Input.GetMouseButtonDown( MouseButton.Left ) && hit.Collider.Parent == Sphere ) {
                initialHitPoint = hit.Point; // Store the initial hit point on the sphere
            }

            if( Input.GetMouseButton( MouseButton.Left ) && hit.Collider.Parent == Sphere ) {
                // Get the current mouse position in world space
                // var currentMousePosition = Input.GetMouseWorldPosition();
                var currentMousePosition =  Camera.MainCamera.ConvertMouseToRay( Input.MousePosition ).Position;

                var worldUp = Sphere.Transform.GetWorld().Up;
                
                // Calculate the rotation based on initial hit point and current mouse position
                var rotationAxis = initialHitPoint - currentMousePosition;
                // var angle = Vector3.Angle(sphereTransform.WorldUp, rotationAxis);
                // var rotationDirection = Vector3.Cross(sphereTransform.WorldUp, rotationAxis);
                var angle = Vector3.Angle(worldUp, rotationAxis);
                var rotationDirection = Vector3.Cross(worldUp, rotationAxis);

                // Apply the rotation to the sphere
                sphereTransform.Rotate(rotationDirection, angle);
            }
        }
    }

Sooo, I’m starting to believe that this might not be possible.

I’ve reached this version of the script which starts well on the first couple of rotations, but eventually starts rotating very weirdly:

public class SphereRotator : Script {

    public Actor Sphere;
    
    public bool ClampX;
    public bool ClampY;
    public bool ClampZ;

    private Vector3 _fromVector;
    private Quaternion _fromOrientation;

    /// <inheritdoc/>
    public override void OnUpdate() {
        // Here you can add code that needs to be called every frame
        var pos = Input.MousePosition;
        var ray = Camera.MainCamera.ConvertMouseToRay( pos );

        if( Physics.RayCast( ray.Position, ray.Direction, out var hit ) ) {
            if( Input.GetMouseButtonDown( MouseButton.Left ) && hit.Collider.Parent == Sphere ) {
                _fromVector = hit.Point;
                _fromOrientation = Sphere.Orientation;
            }

            if( Input.GetMouseButton( MouseButton.Left ) && hit.Collider.Parent == Sphere ) {
                var toVector = hit.Point;

                var rotation = Quaternion.FindBetween( _fromVector, toVector );
                var newOrientation = _fromOrientation * rotation;

                var angles = newOrientation.EulerAngles;
                var maxAngle = 45f;
                if (ClampX) angles = new Float3( Mathf.Clamp( angles.X, -maxAngle, maxAngle ), angles.Y, angles.Z );
                if (ClampY) angles = new Float3( angles.X, Mathf.Clamp( angles.Y, -maxAngle, maxAngle ), angles.Z );
                if (ClampZ) angles = new Float3( angles.X, angles.Y, Mathf.Clamp( angles.Z, -maxAngle, maxAngle ) );
                newOrientation = Quaternion.Euler( angles );
                
                Sphere.Orientation = newOrientation;
            }
        }
    }

}

I wonder what you exactly want to make…
Do you want some kind of trackball rotation of objects in arbitrary world space, which is visible in camera?
And your concern is to match the exact point of surface of the target object with the mouse cursor whilest the rotation, because ‘the tutorial has fixed rotation speed multiplier’?

I’m thinking of a game where the game map is represented by the surface of a sphere.

My goal is to be able to drag the map (rotate the sphere), but the user should have the feel that they control the drag very precisely.

Since the camera can zoom in and out, I imagine that having a constant drag speed will make the map scroll faster on the screen when the camera is zoomed in and slower when the camera is zoomed out.

Ideally, on a tiled hex sphere, when the user clicks on a tile and drags around, wherever they let go, the same tile that was under the mouse when they started would also be under the mouse when they stop.

1 Like

How about thinking about this in a different view/thought. ‘Thinking outside of the box’. This will certainly solve your design issue and also open up to solving a wide variety of rotational and lerp issues. Also reduce bugs for this sort of stuff to zero.

Note after typing: I truly did not mean for this much text but…details right? Right! :blush:

Alright…let’s dig in.

I do this on many things that need to roll about or float and in general it is part of the framework in the namespace (see bottom utility functions).

The Concept:

  • Same as what you are trying to do but…in a Non-Direct method. Meaning the mouse is not actually rotating the sphere. The sphere is ‘listening’ to the mouse after a couple of pre-checks.

  • Clicking down with a raycast check on the object in question. This object has a script that is fast but is only used when being raycasted (Used). Essentially an Interactive Object.

  • You should have a list of Interactable Objects in your framework then simply check what type it is (the type is in the script on the UO). Can even simply set the object types as constant ints in your namespace and simply check the int on the raycast. This is a super fast way to have hundreds of object types. btw…NWN1 uses this method in their nwscript.NSS. I did this in Unity and essentially copied and pasted it into Flax. Works perfectly. Notice below that this is binary. Basically shifting bits to the left in the int. These are Int16 but we have full Int32. There is no real need of binary bits in our case so simply assigning numbers to the object type is enough. Give some spacing in case you want to insert object types in your table.

Example from NWN1:
int OBJECT_TYPE_CREATURE = 1;
int OBJECT_TYPE_ITEM = 2;
int OBJECT_TYPE_TRIGGER = 4;
int OBJECT_TYPE_DOOR = 8;
int OBJECT_TYPE_AREA_OF_EFFECT = 16;
int OBJECT_TYPE_WAYPOINT = 32;
int OBJECT_TYPE_PLACEABLE = 64;
int OBJECT_TYPE_STORE = 128;
int OBJECT_TYPE_ENCOUNTER = 256;
int OBJECT_TYPE_LIGHT = 512;
int OBJECT_TYPE_PLACED_EFFECT = 1024;
int OBJECT_TYPE_ALL = 32767;
int OBJECT_TYPE_INVALID = 32767;

  • Once the Click Down is True && Interactive Object Type Usable ‘Planet’ is True…script is waiting for mouse movement. Notice there is absolutely no requirement to keep the mouse over the sphere/planet. As long as you keep the mouse down and move left/right the sphere will rotate. Release the mouse then you will have to have the mouse slightly over the object again to go through the check.

  • Arrow keys can be used to do the same thing as long as the mouse check has the object in the crosshairs.

Steps do this:

  • no need for Quaternions. You will not end up with gimbal lock. The script is fast enough to check if the current angle of rotation is greater than 1.000 deg (360.000 deg). Rotate Z on the delta time and check for overage.

  • If RotZ>1.000 then Subtract 1.000 and Add the residual to 0.000. Set the RotZ. If using Euler angles then use 360.000 instead.

  • The granularity of the Rotation Angles per frame is so small that simply doing a math check is enough unless spinning the sphere (axis) so ridiculously fast to get into the Frame to Frame delta. In this case you should never even come close.

  • You can even set a clamp (you should) for Maximum Rotation Angles per frame or per second divided by the current CPU frames (variable but more accurate). Lerp this over delta and it is super smooth (I use a dampening formula and not the actual Lerp. The end result is similar though.)

  • At the end you just want the sphere to spin left and right. Set the clamp slow-ish then speed it up at runtime to get the feel you need.

Bonus:

  • You can even use this method to ‘Auto Rotate’ anything without worrying about gimbal lock.

  • In your case maybe you mouse over a Hex then the planet inside the hex starts to spin on its own while hovering on the hex. The map. Then zoom in on this Hex Sector then in your Sensor Display you can mouse click and smooth rotate the planet. You can even expand upon this and have a couple of UI buttons to auto rotate and choose the rotation speed. Just tossing out ideas I already do in my space game that is currently shelved. :rofl: :neutral_face: :roll_eyes:

  • Ooo. Oooo. :rofl: You can even use this to properly rotate space ships with no worries and very accurate thruster push and gravitational pull. No Physics!! HUGE space with NO Physics resolution issues! Whooo Hoooo! I digress. :smiley:

  • In fact make a utility function for this in your namespace and use it over and over. Even add a couple parameters to the function to make it behave slightly different…maybe rotate on the X (ie vehicle wheel or something rolling down a hill). You can even add all 3 axis for some crazy stuff. :grinning_face_with_smiling_eyes:

Cheers
O

A couple of Math Based Utility Functions. Play with these and you will like them. A lot of utility use! This will make your rotations extremely smooth.

You will need to place both of the following in a public static class.
In my case:
gIO_Math.cs
namespace gGenesys
public static class gIO_Math

Dampening (Smoothing) Formula: Similar to finding the Derivative in Calculus
This one is based upon choosing a ‘level’ of dampening on how more/less reactive you want the calculated value to move. You will notice that the Dampening/Smoothing Rate always equals 1.0. 0.1f reacts fast where 0.99999f reacts very slowly. I am also doing decimal point precision error correction in this.

Important Note: The Dampening Rate does not need to equal 1.0. Asymmetric dampening/smoothing can be done for interesting curves but anomalies can be expected. Something else to note here is that you can change the Dampening Rate/Level as needed to create very interesting acceleration and deacceleration scenarios. Maybe after 1.2s change from level 4 to 3 to speed up the rate change per delta then maybe 0.8s later set level 2. Then when getting close to where is need then set level 3 then back to level 4 to finish off the lerp.

// fTarget = Decimal Value needing to be at
// fInput = Current Decimal Value (basically the previously calculated fVar)
fVar = ((fDampenRate * fTarget) + ((1.0f - fDampenRate) * fInput));

public static float DampenFloatByLevel (float fInput, float fTarget, int nDampenLevel)
        {
            float fVar = 0.0f;
            float fValue = 0.0f;
            int nConv = 0;

            // Dampening Level 1
            if (nDampenLevel == 1)
            {
                fValue = (0.1f * fTarget) + (0.9f * fInput);
            }

            // Dampening Level 2
            if (nDampenLevel == 2)
            {
                fValue = (0.01f * fTarget) + (0.99f * fInput);
            }

            // Dampening Level 3
            if (nDampenLevel == 3)
            {
                fValue = (0.001f * fTarget) + (0.999f * fInput);
            }

            // Dampening Level 4
            if (nDampenLevel == 4)
            {
                fValue = (0.0001f * fTarget) + (0.9999f * fInput);
            }

            // Dampening Level 5
            if (nDampenLevel == 5)
            {
                fValue = (0.00001f * fTarget) + (0.99999f * fInput);
            }

            // Get rid of floating point precision errors (3 decimal used)
            fVar = fValue * 1000.0f;
            nConv = (int)fVar;
            fVar = (float)nConv * 0.001f;

            return fVar;
        }

Non-Engine Based Linear Interpolate:

// Dampens a Value per Scan Rate (Similar to Mathf.Lerp in Unity but outside of internals)
        //      This also has no issue with interpolating very large numbers.
        // Note: Raw math linearity
        public static float LinearInterpolate (float fInput, float fTarget, float fSpeedPercent)
        {
            float fVar = 0.0f;
            float fDampenRate = 0.01f;
            int nConv = 0;
            float fInputMin = 0.01f;
            float fInputMax = 99.99f;
            float fScaleMin = 0.0001f;
            float fScaleMax = 0.025f;

            // Inputs
            float fInputRange = fInputMax - fInputMin;

            // Scale
            float fScaleRange = fScaleMax - fScaleMin;

            // Rate Per Scale
            float fRate = fScaleRange / fInputRange;

            // Output
            if (fSpeedPercent < fInputMin)
            {
                fSpeedPercent = fInputMin;
            }
            if (fSpeedPercent > fInputMax)
            {
                fSpeedPercent = fInputMax;
            }
            float fOut1 = (fSpeedPercent - fInputMin) * fRate;
            float fOut2 = fOut1 + fScaleMin;
            fDampenRate = fOut2;

            // Linerally Add Per Scan
            fVar = fInput + fDampenRate;
            if (fVar > fTarget)
            {
                fVar = fTarget;
            }

            // Get rid of floating point precision errors (3 decimal used)
            fVar = fVar * 1000.0f;
            nConv = (int)fVar;
            fVar = (float)nConv * 0.001f;

            return fVar;
        }

Ah, I understood.

Firstly, aside of actual ‘rotation code’, your concern seems like a matter of angular diameter of object. When it is ‘zoomed out’. its angular diameter gets smaller, so rotating same amount of globe map moves a certain point on the surface smaller amount of screen pixels.
(Is it right?).

But by the way I want to be cautious of the idea of ‘syncing’ the amount of input displacement value to the visible amount of pixel movement. It is even worse if you want ‘exactly sync’ the mouse cursor point to the globe map , because the edge of the sphere (horizon) has infinite derivative, and not comfort to control around at all.

So the easiest way to achieve what you said is simply multiplying the angular diameter of target object to the rotation multiplier. Getting angular diameter is trivial, which is inversely proportional to the square of distance from the camera to the target object.

Secondly, as mentioned above, rotation is much more elegant(for user experience) with simple linear rotation which is proportional to the input delta. Imagine pushing a transparent panel to the target sphere and moving the panel to roll the sphere. It is the trackball rotation. There are plenty of sample codes over the web with a veriety of languages.

So…

  1. Just raycast only once when mouse is down, and check you picked the rotatable object.
  2. Store the original coordinate of mouse, and compare it with the ‘current’ coordinate while mouse is down.
  3. You can make the rotation linear using
    Quaternion.Euler( clamped Y, clamped X, 0 )
    referring to the arcball rotation in the Springarm example document,
    or try ‘limited’ rotation using
    Quaternion.FindBetween( original mouse vector, currentouse vector )
    .

I know this suggestion is not 100% same as what you said, But I think it is ‘control friendly’ to the player.

1 Like

I think there are some your own private codes, which is not presented here, not presumable trivially…

@Ray_Rune is saying the same thing I did. Yes, this is the best method. The zoom has nothing to do with the rotation. Rotation is rotation. Simply track the mouse location left/right and let the linear interpolate handle the spin. This is very easy to do and is very player/designer friendly.

Tip:
*A small additional note on the script/object communications. You will want to make a general use scene object (not rendered at 0,0,0) that handles general scene stuff. Like tracking the mouse position, mouse click, keyboard stuff…time…scene stuff. Listener Objects will cache this/these objects and listen for the data they will need. In this case the Sphere/Planet needs Mouse Down and Left/Right info. It will always be listening (some call these events but works differently). It is important to set up this early in design to make things easier down the road. It is very much worth the time doing this prior to testing things/ideas. Flax has some Find Object/Actor options (a lot to be honest) and there is a forum topic asking which is best. Use that to store/cache the object reference for easy access. Only need to do this if the OBJECT_TYPE_INVALID check is True (Object is null). And yes sometimes the memory cache gets emptied so you might need to Find again during runtime. *

Quote: I think there are some your own private codes, which is not presented here, not presumable trivially…

No. Those are the entire functions and everything within them is math. They need to be in a static class. Maybe you have never done this before?

I will be explicit below if someone reading this is not familiar with doing this sort of thing. If you have not, it is super powerful, saves enormous amounts of programming time, and once tested zero bugs ever. Can mix and match your parameters. In fact the general programming rule is…if you are using the same code and pasting it more than twice then the code logic should go into a function with parameters.

C++ will be similar, but I am not using the C++ side yet.

C# Ref:

The method parameters can be a little confusing but the examples are there. The examples are not combining the class being located in the namespace keyword. Also try to avoid the nested namespace.

It is a good idea to make your namespace (‘Include’) scripts have a unique prefix so they are not confused with internal engine functions/features. Sometimes the name of the function can be similar. In my case everything is prefixed with a g for Genesys Framework. The functions will always have the gScript.Function format so it is very easy to know where the function is coming from.

I am not sure what the limits are on parameters, but it is a lot.

Script.Function(Parameter1, Parameter2, … , Parameter10)

script name: gIO_Math.cs
namespace gGenesys
{    
    public static class gIO_Math
    {
        // Some Float Function with Parameters
        public static float CustomFloatFunction(float fVariable1, float fVariable2, int nVariable3)
        {
             return fVal;
        }

        // Some Int32 Function with Parameters
        public static int CustomInt32Function(int nVariable1, int nVariable2, float fVariable3)
        {
             return nVal;
        }
        
        // Some non-static class within the declared public static class (include script)
        public class Timer
        {
            public float SomeTimerFloat
            {
                get
                {
                    
                }
                private set
                {
                    
                }
            }            
        }
    }
}

Calling these functions in a script:
At the top of each script you need your namespace.

using FlaxEngine;
using gGenesys;

Above functions as example.
fRotZ = gIO_Math.DampenFloatByLevel(1.123, 2.123, 3);
1.123 = Should be some variable like Rotational Value (it is a Float)
2.123 = Same. Should be the Rotational Angle Target variable (Mouse Left/Right)

That is enough blabber and details from me. I hope it help and clarifies some things.
Cheers
O

1 Like

if you say “gIO_Math.cs” and there is no explicit designation of ‘what snippet is “gIO_Math.cs”’, no, it is not mentioned. And I see all the codes presented was only for calculating the dampening for the interpolation, than the actual routine to achieve something that OP said.

You can be right because many words of you were too vague for me, so your explanation can be ‘same as what I said’ because I couldn’t get what it was.

‘It can be achieved with interpolations’ right.
‘interpolations methods can be globally called from static class’ right.
‘Never Winter Night script used interactable object type bit field’ okay.

And I couldn’t read the trackball rotation suggestion what OP concerns from that, so I wrote what I can suggest with my words.

I too don’t want this thread to be chatter between OlanderDA and me, not for the OP. I will stop.
Whether I understood your explanation or not is not important for this thread, so I hope OP can merge the idea of us two and solve the problem.

Okay, one last time. Not sure what is so confusing about the script and namespace?

Simple mode version. For details see above. This is very related to getting proper linear motion working. Also no physics stuff required.

  • Make a C# script. Name it gIO_Math. Compile. Save. (Name this whatever your Math Include will be)

Note: include is a C and C++ thing. C-Sharp uses a Keyword instead. It took me a few tries to understand the difference since I was programming in C long ago.

  • Add your namespace at the top. I gave my example of what I use for my framework. namespace gGenesys or SomeNamespace. Curly bracket below it and compile. Namespace created.

  • In your Sphere/Planet script that will do the Rotation. Make sure to have the using directive under the Flax engine directive like so.
    using FlaxEngine;
    using gGenesys;

  • Add your class within the namespace. Whatever class you want. What I recommend is making the class name the same as the script name. This helps later to keep track of where things are being called from.

When calling the function in CS you are not calling the script from another script. You are calling the function in the namespace…which just happens to be in the script named gIO_Math.cs this script name could be SomeTestingScript.cs and the function will still be the same.

And this is where I am never returning to the Flax Forums. Thanks. Bowing out. Had enough. I am almost 58 now and giving out good advice is a pleasure but not with the face pounding the wall as above. Delete all my stuff/posts if you folks want to. I guess I simply no longer have the patience for this.

Thank you for the replies. This is a hobby project, so I don’t have time to try them right now but I’m reading through the comments and I’ll give it a go as soon as I can.

1 Like

So I haven’t been able to achieve exactly what I wanted, but I got close enough with the Spring Arm Camera.

The problem was indeed that, at a constant MouseSpeed, the sphere will rotate faster the closer I zoom in. Well, not actually faster, but it will seem faster on the screen because of the smaller perspective.

I was able to correct this issue by correlating the movement speed with the distance of the Camera from the Target.

Here is my modified script.

    public override void OnUpdate() {
        if( !CameraActor || !TargetActor )
            return;

        Distance += Input.MouseScrollDelta * -20.0f * MouseWheelSpeed * ( Distance / 1000 );
        Distance = Mathf.Clamp( Distance, MinDistance, MaxDistance );

        if( Input.GetMouseButton( MouseButton.Right ) ) {
            // Update input
            _mouse += Input.MousePositionDelta * MouseSpeed * ( Distance / 1000 );
            _mouse.Y = Mathf.Clamp( _mouse.Y, -89, 89 );
        }

        // Update arc-ball camera
        Quaternion rotation = Quaternion.Euler( -_mouse.Y, _mouse.X, 0 );
        Vector3 targetPosition = TargetActor.Position;
        Vector3 direction = Vector3.Transform( Vector3.Forward, rotation ) * Distance;
        Vector3 newPosition = targetPosition + direction;
        direction = ( newPosition - targetPosition ).Normalized;
        CameraActor.Position = targetPosition + direction * Distance;
        CameraActor.LookAt( targetPosition, Vector3.Up );
    }