In the image above
the red vector is the spider's forward vector
the blue vector is the vector representing the direction between the spider and it's target
In the code below, orientation is a vector that's representing the normal of the terrain, so that the spider gets aligned to it:
Vector3 orientation = GetTerrainNormal();
Quaternion rotationNeeded = Quaternion.FromToRotation(Vector3.up, orientation);
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
rotationNeeded,
RotationSpeed * Time.deltaTime
);
My issue is that I cannot manage to make the spider face its target... When I add any code that would make it rotate towards it, then it's not aligned with the terrain's normals anymore, it says straight...
So basically, how can I make the spider rotate on the Y world axis (I think), while still then being rotated to match the slope?
Full answer
In case it helps someone else, here's the full answer:
Vector3 orientation = GetTerrainNormal();
Vector3 directionToTarget = (target.position - transform.position).Y(0);
float d = Vector3.Dot(directionToTarget, orientation);
directionToTarget -= d * orientation;
if (directionToTarget.sqrMagnitude > 0.00001f) {
directionToTarget.Normalize();
Quaternion rotationNeeded = Quaternion.LookRotation(directionToTarget, orientation);
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
rotationNeeded,
xRotationSpeed * Time.deltaTime
);
}
This answer on the unity forums was extremely helpful: https://forum.unity.com/threads/look-at-object-while-aligned-to-surface.515743/
Try this
Vector3 directionToTarget = target.transform.position - transform.position;
Quaternion rotationNeeded = Quaternion.LookRotation(directionToTarget, orientation);
First of all, I'm not sure why you need a code to orient the spider manually to the terrain. You can make the spider a Rigidbody and the Unity engine will take care of it for you.
Regardless, you want to rotate the spider around the local Y-Axis (this will keep the current orientation).
You can do this using transform.LookAt() (referring to the blue vector in the picture) (documented here) and passing the up vector as the 2nd argument.
Related
First off: I am very new to Unity, as in VERY new.
I want to do the following: I want to rotate a cube around a stationary point (in my case a camera) with a radius that is adjustable in the inspector. The cube should always have its Z-axis oriented towards the camera's position. While the cube is orbiting around the camera, it should additionally follow a sine function to move up and down with a magnitude of 2.
I have some working code, the only problem is an increase in distance over time. The longer the runtime, the higher the distance between the cube and the camera.
Here is what I currently have:
void Awake()
{
cameraPosition = GameObject.FindGameObjectWithTag("MainCamera").transform;
transform.position = new Vector3(x: transform.position.x,
y: transform.position.y,
z: cameraPosition.position.z + radius);
movement = transform.position;
}
I instantiate some variables in the Awake()-method and set the cube's position to where it should be (do you instantiate in Awake()?). I'll use the Vector3 movement later in my code for the "swinging" of the cube.
void Update()
{
transform.LookAt(cameraPosition);
transform.RotateAround(cameraPosition.position, cameraPosition.transform.up, 30 * Time.deltaTime * rotationSpeed);
MoveAndRotate();
}
Here I set the orientation of the cube's z-axis and rotate it around the camera. 30 is just a constant i am using for tests.
void MoveAndRotate()
{
movement += transform.right * Time.deltaTime * movementSpeed;
transform.position = movement + Vector3.up * Mathf.Sin(Time.time * frequency) * magnitude;
}
To be quite frank, I do not understand this bit of code completely. I do however understand that this includes a rotation as it moves the cube along it's x-axis as well as along the world's y-axis. I have yet to get into Vector and matrices, so if you could share your knowledge on that topic as well I'd be grateful for that.
It seems like I have found the solution for my problem, and it is an easy one at that.
First of all we need the initial position of our cube because we need to have access to its original y-coordinate to account for offsets.
So in Awake(), instead of
movement = transform.position;
We simply change it to
initialPosition = transform.position;
To have more readable code.
Next, we change our MoveAndRotate()-method to only be a single line long.
void MoveAndRotate()
{
transform.position = new Vector3(transform.position.x,
Mathf.Sin(Time.time * frequency) * magnitude + initialPosition.y,
transform.position.z);
}
What exactly does that line then? It sets the position of our cube to a new Vector3. This Vector consists of
its current x-value
our newly calculated y-value (our height, if you want to say so) + the offset from our original position
its current z value
With this, the cube will only bop up and down with distancing itself from the camera.
I have also found the reason for the increase in distance: My method of movement does not describe a sphere (which would keep the distance the same no matter how you rotate the cube) but rather a plane. Of course, moving the cube along a plane will automatically increase the distance for some points of the movement.
For instantiating variables in Awake it should be fine, but you could also do it in the Start(){} Method that Unity provides if you wanted to.
For the main problem itself I'm guessing that calling this function every frame is the Problem, because you add on to the position.
movement += transform.right * Time.deltaTime * movementSpeed;
Would be nice if you could try to replace it with this code and see if it helped.
movement = transform.right * Time.deltaTime * movementSpeed;
In the GIF above I am moving the plane with the mouse up and down. Watch how when I rotate the plane on its Z axis the plane still moves up and down even if the plane is side ways now. That is my bug. Why is this happening? What is wrong with my code? I think I am using the global axis and not the gameobjects axis. How do I fix this please.
x += Input.GetAxis("Mouse X") * xMouseSpeed * 0.02f;
y -= Input.GetAxis("Mouse Y") * yMouseSpeed * 0.02f;
if (Input.GetKey(KeyCode.Q))
{
rotZ += Time.deltaTime * 50f;
}
if (Input.GetKey(KeyCode.E))
{
rotZ -= Time.deltaTime * 50f;
}
Quaternion rotation = Quaternion.Euler(y, x, rotZ);
planeBodyToMove.localRotation = rotation;
What happens is that the localRotation is just the rotation in relation to the parent object.
The rotation of the transform relative to the transform rotation of the parent.
Or in other words: The localRotation is not local to the object itself but rather a rotation in the local space of the parent (or Unity world space if there is no parent). It does not mean that you rotate around your local Y axis necessarily. It applies the rotations in the parent's local space in the order Z, X, Y .. that is why sometimes you actually don't note the difference until you rotate Y!
As you can see here the same thing happens also without your code by just doing the rotation via the Inspector
Or to demonstrate it a bit better lets see what happens if we rotate it the way you actually want it to rotate:
as you can see as soon as one axis already is rotated, changing another axis now changes all three rotation values.
Solution
So how to solve this?
Usually I'm to lazy to do Quaternion calculations myself → I let the Transform do this for me ^^
Instead of storing the expected rotation in fields and apply it everytime by converting it into a Quaternion you can directly use Transform.Rotate which rotates the object actually using its own local axis:
// Just a question of taste but I would prefer to have
// values that belong together in one single struct instead of multiple fields
public Vector2 mouseSensitivity = Vector3.one;
private void Update()
{
var xDif = Input.GetAxis("Mouse X") * mouseSensitivity.x;
var yDif = - Input.GetAxis("Mouse Y") * mouseSensitivity.y;
var zDif = 0f;
if (Input.GetKey(KeyCode.Q))
{
zDif= Time.deltaTime * 50f;
}
else if (Input.GetKey(KeyCode.E))
{
zDif= -Time.deltaTime * 50f;
}
// Wihout the optional space parameter uses local space by default
planeBodyToMove.Rotate(new Vector3(xDif, yDif, zDif));
}
As you can see now each rotation is actually applied in the plane's local axis:
I am trying to archive without success a z-axis rotation movement around a moving object keeping always "looking at front". So it should looks like this:
The closest I got was with:
transform.RotateAround(targetPosition, Vector3.forward, moveSpeed);
But it does not keeps looking "at front".
Could someone give me a hand with this?
Thank you in advance.
Best regards.
If your object ("Lightning Bolt") has no world rotation, i.e. aligned with the world axis as your example image seems to suggest, then the easiest is to simply set the world rotation to the Quaternion Identity:
transform.rotation = Quaternion.identity;
Note that the image wont rotate if its parent object rotates. It will essentially "Billboard" your lightning object. If you want to your lightning bolt to be aligned with a parent object, then try something like:
transform.rotation = transform.parent.rotation;
Fitst store the current rotation, then rotate around point, lastly apply the previous rotation.
var rot = transform.rotation;
transform.RotateAround(targetPosition, Vector3.forward, moveSpeed);
transform.rotation = rot;
A simple solution would just manipulate actual coordinates and ignore rotation ^^ If an object moves and you want it to keep rotating around it, just make it a child object.
This is a 3d solution where we rotate around Y:
void Start() { angle = 0.0f }
void Update() {
angle += speed * Time.deltaTime; //Your starting angle that will be modified every frame
CheckAngle(ref angle);
float x = Mathf.Cos(angle * Mathf.Deg2Rad) * Radius;
float z = Mathf.Sin(angle * Mathf.Deg2Rad) * Radius;
}
static void CheckAngle(ref float Angle) //It should probably return a value by it's name here tho not ref, not sure which is the correct way of doing this
{
if (Angle > 360) //You don't want your angle to go past the boundaries of an INT
Angle = 0;
}
Suppose you have a camera projection matrix, i.e. camera translation vector + rotation quaternion, like every typical camera, it is able to move and rotate in any direction. And independent of it's rotation whether it is looking forward, upward or downward I need to show a compass-like gauge pointing where the camera is targeted at.
The problem is that when the camera is pointed downwards the rotation of camera around it's optical center defines the value of the compass, but when the camera points forward, the rotation of camera around it's center no longer affects the value of compass, in this case the direction of camera defines the value of compass.
It get's more ugly when the camera is tilted downwards only 45 degrees, in this case it is not even clear whether the rotation around camera center affects rotation of compass.
So is there an elegant way of getting the compass value based on arbitrary camera projection matrix / quaternion?
Thank you in advance!
If you want just an arrow pointing at the target its:
Transform camera = Camera.main.transform;
Transform target = Target.transform;
Vector3 relativePosition = target.position - camera.position;
Vector3 targetRelative = Vector3.ProjectOnPlane(relativePosition, camera.forward);
float angle = Angle360(camera.up, targetRelative, camera.forward);
Compass.transform.rotation = Quaternion.Euler(0, 0, angle);
The angle function is:
float Angle360(Vector3 from, Vector3 to, Vector3 normal)
{
float dot = Vector3.Dot(from, to);
float det = Vector3.Dot(normal, Vector3.Cross(from, to));
return Mathf.Atan2(det, dot)*Mathf.Rad2Deg;
}
Here is how you can get the direction of the compass in worldspace:
Project the camera direction and target position on the XZ plane
Transform camera = Camera.main.transform;
Transform target = Target.transform;
Vector3 cameraWorldDirXZ = Vector3.ProjectOnPlane(camera.forward, Vector3.up).normalized;
Vector3 targetWorldDirXZ = Vector3.ProjectOnPlane(target.position, Vector3.up).normalized;
The angle between the cameraWorldDirXZ and targetWorldDirXZ is the angle of your compass needle.
But i don't think this will behave like you think it will. This gives you the angle that you need to rotate the camera.forward vector around the y axis to face the target. If you rotate around camera.forward you don't change either the camera.forward vector or the y axis so the compass wont change.
You might want to try a compass in local space. For that you project onto the camera XZ plane:
Vector3 cameraLocalDirXZ = camera.forward;
Vector3 targetLocalDirXZ = Vector3.ProjectOnPlane(target.position, camera.up).normalized;
Again the angle between the cameraLocalDirXZ and targetLocalDirXZ is the angle of your compass needle. This gives you the angle you need to rotate camera.forward around camera.up to face the target. Note that when you rotate around camera.forward it will change camera.up so it will change the compass direction.
If anyone stumbles upon this problem, the solution (thanks to #Pluto) is very simple, multiply your camera quaternion over three axis vectors (0,0,1), (0,1,0), (1,0,0), you will get three vectors defining coordinate system of your camera, project those three vectors onto your plane, find centroid of your three projected points and voila you have compass direction.
Here's the piece of code for that:
var rotation = /* Your quaternion */;
var cameraOrtX = rotation * new Vector3 (1, 0, 0);
var cameraOrtY = rotation * new Vector3 (0, 1, 0);
var cameraOrtZ = rotation * new Vector3 (0, 0, 1);
var cameraOrtPX = Vector3.ProjectOnPlane(cameraOrtX, new Vector3(0, 1, 0));
var cameraOrtPY = Vector3.ProjectOnPlane(cameraOrtY, new Vector3(0, 1, 0));
var cameraOrtPZ = Vector3.ProjectOnPlane(cameraOrtZ, new Vector3(0, 1, 0));
var centroid = (cameraOrtPX + cameraOrtPY + cameraOrtPZ) / 3.0f;
I am using the following code to handle rotating my player model to the position of my mouse.
void Update() {
// Generate a plane that intersects the transform's position with an upwards normal.
Plane playerPlane = new Plane(Vector3.up, transform.position);
// Generate a ray from the cursor position
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// Determine the point where the cursor ray intersects the plane.
// This will be the point that the object must look towards to be looking at the mouse.
// Raycasting to a Plane object only gives us a distance, so we'll have to take the distance,
// then find the point along that ray that meets that distance. This will be the point
// to look at.
float hitdist = 0f;
// If the ray is parallel to the plane, Raycast will return false.
if (playerPlane.Raycast(ray, out hitdist)) {
// Get the point along the ray that hits the calculated distance.
var targetPoint = ray.GetPoint(hitdist);
// Determine the target rotation. This is the rotation if the transform looks at the target point.
Quaternion targetRotation = Quaternion.LookRotation(targetPoint - transform.position);
// Smoothly rotate towards the target point.
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, speed * Time.deltaTime); // WITH SPEED
//transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, 1); // WITHOUT SPEED!!!
}
I would like to be able to determine if the rotation is clockwise or counter-clockwise for animation purposes. What would be the best way of handling this? I'm fairly unfamiliar with quaternions so I'm not really sure how to approach this.
Angles between quaternions are unsigned. You will always get the shortest distance, and there's no way of defining "counter-clockwise" or "clockwise" unless you actively specify an axis (a point of view).
What you CAN do, however, is to take the axis that you're interested in (I assume it's the normal to your base plane.. perhaps the vertical of your world?) and take the flat 2D components of your quaternions, map them there and compute a simple 2D angle between those.
Quaternion A; //first Quaternion - this is your desired rotation
Quaternion B; //second Quaternion - this is your current rotation
// define an axis, usually just up
Vector3 axis = new Vector3(0.0f, 1.0f, 0.0f);
// mock rotate the axis with each quaternion
Vector3 vecA = A * axis;
Vector3 vecB = B * axis;
// now we need to compute the actual 2D rotation projections on the base plane
float angleA = Mathf.Atan2(vecA.x, vecA.z) * Mathf.Rad2Deg;
float angleB = Mathf.Atan2(vecB.x, vecB.z) * Mathf.Rad2Deg;
// get the signed difference in these angles
var angleDiff = Mathf.DeltaAngle( angleA, angleB );
This should be it. I never had to do it myself and the code above is not tested. Similar to: http://answers.unity3d.com/questions/26783/how-to-get-the-signed-angle-between-two-quaternion.html
This should work even if A or B are not Quaternions, but one of them is an euler-angle rotation.
Two dimensional quaternions (complex numbers) have a signed angle. But, the more correct way to think about complex numbers is with an unsigned angle which is relative to either the XY oriented plane or the YX oriented plane. I.E. a combination of an unsigned angle an an oriented plane of rotation.
In 2D there are only two oriented planes of rotation so the idea of a "signed angle" is really just a trick to get both the unsigned angle and the oriented plane of rotation packed into a single number.
For a quaternion the "signed angle" trick cannot be used because in 3D you have an infinite number of oriented planes you can rotate in, so a single signed angle cannot encode all the rotation information like it can in the 2D case.
The only way for a signed angle to make sense in 3D is with reference to a particular oriented plane, such as the XY oriented plane.
-- UPDATE --
This is pretty easy to solve as a method on a quaternion class. If all you want to know is "is this counter clockwise", then since we know the rotation angle is from 0 to 180, a positive dot product between the quat's axis of rotation and the surface normal should indicate that we're rotating counter clockwise from the perspective of that surface. And a negative dot product indicates the opposite. Ignoring the zero case, this should do the trick with much less work:
public bool IsCounterClockwise( in Vector3 normal ) => I*normal.X + J*normal.Y + K*normal.Z >= 0;