- Home /
Do we have a fix for the -x scale bug?
By "-x scaling bug" I mean an issue I've seen on the forums a lot with no solution. (Here's my question about it from feb 2014)
The bug is pretty simple to explain.
I have a parent GO. Under it is a hierarchy of other GO's most of which are Quads of body parts, forming a character. I'm multiplying the parent GO's scale.x by -1 to face left and right, and it works fine.
If I try to rotate a child GO, however (say, to point an arm in a direction) its rotation doesn't work if the parent's scale is -1. Note all of this rotation happens on the z-axis, since its a 2D game.
Heres a visual to help:
I'm confident it's not a logically fallacy in my code, and I had a workaround just doing every frame GO.SetActive(false); GO.SetActive(true); However it is an expensive operation, so I eventually discovered that I get a similar effect doing 2 x-flips each frame. The issue with that is the arm 'wiggles' as if it's doing the wrong thing, then getting fixed... (actually I might be able to fix that with script exe order)
Anyway, I'd love to stop having to do workarounds. From the image it seems that the child objects' transformed positions are correct (gun moves up) but the rotations are not (gun points down).
Can anyone shed some light on why this happens? If there is a way to design it that won't break? Is there an efficient way to 'recalculate' child transforms?
Thanks as always, I hope I can get some good suggestions!
O$$anonymous$$, now I shouldn't have to defend my code anymore. Here's a super-simple test case. Set up a scene like the given image:
The main object is the GO->GO(w/script)->Quad. The GO's are both at 0,0,0 and the quad is at (-0.25,-0.25,0) (to center it on the 'shoulder' part of the image) The scale of all objects is 1,1,1. The two top-level quads are just to form the crosshairs.
The only script needed is this:
class RotateToAngle : $$anonymous$$onoBehaviour
{
public float TargetAngle = 0;
public float DegreesPerSecond = 180;
void Update()
{
float dAng = $$anonymous$$athf.DeltaAngle(transform.localRotation.eulerAngles.z, TargetAngle);
if (dAng == 0)
return;
float moveAng = $$anonymous$$athf.Sign(dAng) * $$anonymous$$athf.$$anonymous$$in(Time.deltaTime * DegreesPerSecond, $$anonymous$$athf.Abs(dAng));
transform.localRotation *= Quaternion.Euler(0, 0, moveAng);
}
}
Now, run it and change the TargetAngle in the script. Play with it a bit and watch the 'shoulder' stay centered in the crosshairs. Then set TargAng=0, and change the top GO's scaleX to -1. Change TargAng some more - the shoulder moves far away from the crosshairs. This is the bug, unless my understanding of transform math is very wrong. Please explain.
EDIT: Since it's hidden in a comment below I'll add it here... See this example project for a simplified look at the bug. https://drive.google.com/file/d/0B2xyY-B7j1PqblJ6SG9D$$anonymous$$HJ5TVE/view?usp=sharing
You don't need to "defend" your code... No one attacks you. We just try to help you...
Anyway...
Why do you use quads and not sprites ? Did you try with sprites ?
Well I did say "I'm confident it's not a logically fallacy in my code" so I've now defended that statement.
$$anonymous$$y game already has dozens of quad-based prefabs, and hundreds - literally - of scripts all written for a 3D, quad-based world. I started it over a year ago, and there's no going back now.
I've had it for the past year of development (I started Unity about 1.5 yr ago), but I keep using different workarounds which create problems once in a while. I was just hoping it would be fixed in 4.6, or that someone would be able to explain why this would happen.
Answer by callen · Feb 18, 2015 at 07:51 PM
Ok until someone can give an actual answer, just use the hack I described in the comments. Essentially, be doing two back-to-back x-flip operations on an x-flipped parent, the rotations will work as expected (mirror of what you get without the flip).
//somewhere in the parent
void Update(){
//technically you only have to do this when you are in an x-flipped state
if(transform.localScale.x > 0)
return;
Vector3 flipX = new Vector3(-1,1,1);
//flip it
transform.localScale = Vector3.Scale(transform.localScale, flipX);
//flip it back
transform.localScale = Vector3.Scale(transform.localScale, flipX);
}
It's looking like it's a unity bug, but one that's pretty easy to work around, so whatever.
Answer by carrollh · Jan 16, 2015 at 07:27 AM
EDIT: I knew something was funky. What you actually want to do is ROTATE your top GameObject by 180 around the y-axis, not scale it by -1 in the x. Just add another quad that is already flipped over so your gun quad is effectively double-sided. Then you can totally ignore the script mod below.
I'm not 100%, but I don't think this is a bug. This looks similar to what happens if you do a Scale*Rotate*Translate out of order. Look here and here. I'll work on the maths to prove it to you, but I'm going to have to take a picture of the actual matrix multiplications because my patience isn't up to typing all that in :P
Personally the fact that you are scaling by -1 1 1 makes my skin crawl a bit. It works when mirroring a mesh in Maya for symmetry, but it doesn't sound like a good idea at run time.
To get the effect you actually want, you can use this modified version of your script above: (WARNING: if you set isFlipped to true, you'll want to zero out your rotations before setting it back to false or you'll get extra rotational stuff)
using UnityEngine;
using System.Collections;
class RotateToAngle : MonoBehaviour
{
public float TargetAngle = 0;
public float DegreesPerSecond = 180;
public bool isflipped = false;
private bool isAlreadyFlipped = false;
private int targetSign = 1;
void Update()
{
if (isflipped)
{
if (!isAlreadyFlipped) FlipIt();
}
else if (isAlreadyFlipped) FlipIt();
float dAng = Mathf.DeltaAngle(transform.localRotation.eulerAngles.z, targetSign * TargetAngle);
if (dAng == 0)
return;
float moveAng = Mathf.Sign(dAng) * Mathf.Min(Time.deltaTime * DegreesPerSecond, Mathf.Abs(dAng));
transform.localRotation *= Quaternion.Euler(0, 0, moveAng);
}
void FlipIt()
{
isAlreadyFlipped = !isAlreadyFlipped;
Transform quad = transform.GetChild(0);
Vector3 pos = quad.position;
quad.position = new Vector3(pos.x * -1, pos.y, pos.z);
Vector3 scale = quad.localScale;
quad.localScale = new Vector3(scale.x * -1, scale.y, scale.z);
targetSign *= -1;
}
}
If you could explain how the matrix math is wrong I'd appreciate it. But since I'm essentially setting the xflip in one script and the rotation in another (in my production code) I would think unity is supposed to make sure to apply them correctly for rendering. And the part I'm still not understanding is why then, does it work as expected when I just do this:
//on the object which we set the x-scale to -1/1
void Update(){
//flip the object.
transform.localScale = Vector3.Scale(transform.localScale, new Vector3(-1,1,1));
//flip it back to its original orientation.
transform.localScale = Vector3.Scale(transform.localScale, new Vector3(-1,1,1));
}
As for rotating 180 degrees around the Y-axis, I'm not convinced it's a great idea to double my quad-count for anything that I need to face a direction (players (each is ~15 quads), npcs, enemies). I also forsee a lot more issues with trying to build a complex character out of many duplicated and y-rotated quad hierarchies.
The workaround is working fine for me. But I still wish someone could really explain why this happens, so maybe I could design around it more easily.
Sorry this took so long. Normal transform operations (Scale Rotate Translate) on skinned meshes: From the root onwards all of the positions for each vert belonging to a joint must be calculated by first finding the effective total transforms for all of the joint between it and the root. Each joint transform is calculated as Scale-Rotation-Translation. In games normally the scale is set on the root only, and translates only happen if there is a detachable joint somewhere in the rig. XNA didn't support "non-uniform scaling" (scaling joints besides the root) at all. That said, the "normal" math looks like this:
Vertex Point X Scale $$anonymous$$atrix X Rotation $$anonymous$$atrix X Transform $$anonymous$$atrix
|x| |1 0 0| |Rx 0 0| |1 0 0|
|y| |0 1 0| X |0 Ry 0| X |0 1 0|
|z| |0 0 1| |0 0 Rz| |0 0 1|
So before we "flip" your Quads by doing the -X scale thing, let's look at a point originally at [1 1 0] when rotated 90 degrees about z: Original equation:
|1| |1 0 0| |cos a -sin a 0| |1 0 0|
|1| X |0 1 0| X |sin a cos a 0| X |0 1 0|
|0| |0 0 1| | 0 0 0| |0 0 1|
(and after solving the trig in the rotation matrix):
|1| |1 0 0| | 0 -1 0| |1 0 0|
|1| X |0 1 0| X | 1 0 0| X |0 1 0|
|0| |0 0 1| | 0 0 0| |0 0 1|
Which, doing the math in order from left to right, yields [-1 1 0]. In other words, a point at [1 1 0] will wind up at [-1 1 0] after 90 degree rotation about z, normally.
Now look at what happens if you scale it by -1 in the x first. (What you want is it to have the opposite X value from our first result, right? So you are looking for it to become [1 1 0]. But that is not what happens.) First scale the original point by the root scale matrix
|1| |-1 0 0| |-1|
|1| X |0 1 0| = | 1|
|0| |0 0 1| | 0|
Then perform the normal operations from before:
|-1| |1 0 0| | 0 -1 0| |1 0 0|
| 1| X |0 1 0| X | 1 0 0| X |0 1 0|
| 0| |0 0 1| | 0 0 0| |0 0 1|
You get [-1 -1 0], which is totally not what you want.
But, if ins$$anonymous$$d you rotated it by -90, you would get
|-1| |1 0 0| | 0 1 0| |1 0 0|
| 1| X |0 1 0| X | -1 0 0| X |0 1 0|
| 0| |0 0 1| | 0 0 0| |0 0 1|
Resulting in [1 1 0], which is what you want. This is why rotating the whole thing around Y by 180 works. You don't need code to figure out if you're in a flipped state or not. Because z is now pointing the opposite way, so any rotation will look like it's negative version viewed from above.
Hey thanks for this detailed response, and sorry for my late reply. The math you're showing definitely makes sense, but I'm not sure it applies to this situation. First, idk if there's a fundamental difference between meshes and joints versus unity's parent-child relationships acting on my quads. Possibly related, I would think that if I change any property (Translate, Rotate, Scale) on a parent, ALL of those changes would be applied before calculating the child. Therefore, I would think order of operations should not matter for my case, where a parent is being scaled and a child rotated.
Finally, it creates even more confusion for me regarding my workaround. If the results should match what you're saying, why do I get a different result with (what should be) an idempotent operation of two x-flips on the parent object?
I really do appreciate you taking the time to write all this out. Considering everything we've went over so far, what I'm thinking is this.. the child's local coordinate space is flipped when x=-1, so rotations would not be the same as unflipped. But somehow, when the coordinate space is flipped back to normal (x=1), the angle is applied correctly, and this correctness is preserved when it is flipped back to x=-1.
However that seems to imply unity is recalculating the transforms somewhere unexpectedly, possibly when localScale is changed?
I'm not sure I understand what you mean by "two independent x-flips". Using the math above, if you took a point (1,1,0) and x-flip it, you get (-1,-1,0). If you were to apply another x-flip, the same math would get you to (1,-1,0), which is not the original (1,1,0).
Regarding the "applied to every child" part you were taking about, yes. Any transform you apply to any parent node gets applied to all child nodes. If you want to calculate the absolute position of say a vertex on a finger, you have to apply the matrix transforms from the root all the way to that finger's joint. So if there were 10 joints (including root and finger) you would be doing the whole S*R*T thing 10 times back to back and then adding the local space vector coordinate of the vertex. This is why most game platforms don't allow you to scale joint transforms in the middle of an animation. If they can assume that it's effectively 1 all the time, then they can ignore that matrix multiplication cutting the math down by ~33%. You can go even further if you only allow rotations, thus only needing the matrix rotation multiplications being calculated from the one translation matrix which is the world space coordinate of the root => S*R*T*R*R*R*R*R*R*R*R*R. So while yes, anything you apply to the parent will apply to the children, it happens at runtime and it only happens once in the chain.