How to scale Matrix4x4 around point? - c#

I'm writing a custom editor window in Unity in which I would like to be able to both scroll in/out and drag the view around. To do so, I've been setting GUI.matrix to Matrix4x4.TRS(offset, Quaternion.identity, Vector3.one * scale), where I have control over offset and scale. This works fine, except when scrolling in/out, it anchors the top left of the window. I would like it to anchor on the mouse's position.
If this just requires changing the offset when zooming, that's great - I'm just not sure what the offset should be here. Matrix4x4s are out of my comfort zone for math.
Here is how I'm currently handling zooming:
if (Event.current.type == EventType.ScrollWheel)
{
_scale *= Math.Sign(Event.current.delta.y) == 1 ? 1.1f : 1f / 1.1f;
_offset += Math.Sign(Event.current.delta.y) * /*What do I put here?*/;
}

Let's try to understand whatthe GUI matrix does. It represents a transform that takes coordinates in world space (where your GUI objects live) and converts them to GUI space (more or less aligned with your window). Since we have no rotation, we can easily interpret what constructing the matrix with TRS() does to a world-space point pWorld:
pGUI = scale * pWorld + offset
Now you want to change scale to scaleNew. In doing so, you want to keep the same world position under the mouse.
If your mouse position is given in GUI space (e.g., from Event.current.mousePosition), then we first need to find the corresponding world space point:
v3World = (1.0 / scaleOld) * (v3GUI - offsetOld)
And we want to fix this point under the mouse, i.e.:
v3GUI = scaleNew * v3World + offsetNew
v3GUI = scaleNew / scaleOld * (v3GUI - offsetOld) + offsetNew
We can solve this to get the new offset:
v3GUI = scaleNew / scaleOld * v3GUI - scaleNew / scaleOld * offsetOld + offsetNew
(1 - scaleNew / scaleOld) * v3GUI + scaleNew / scaleOld * offsetOld = offsetNew
And that's it.
Btw, you can also do this with matrix operations alone. This is what GUIUtility.ScaleAroundPivot() does. This is how it looks:
newMatrix = T(v3GUI) * S(newScale / oldScale) * T(-v3GUI) * oldMatrix
T represents a translation and S a scaling. The translation pair T(v3GUI) and T(-v3GUI) move the temporary origin of the coordinate system to your mouse position and perform the scaling from there. You could then directly read offset and scale from this matrix.

Related

Rotating objects on Canvas

I'm working on a rather Large project. It was already finished when I started and I have to implement some small gimics.
One of those is the rotation of a marker on a map.
When the marker is selected a rectangle (System.Wndows.FrameWorkElement) is drawn around the picture. Since I would basically have to rewrite the whole program to use another rectangle, I have to stick with the framework element.
To rotate this thing, I added a line and a circle.
The line connects the circle with the rectangle. When the user clicks on the circle and drags the mouse, the whole thing is supposed to rotate around the center of the rectangle.
So far, the rotation of the rectangle and the line works fine. But the circle, though it is rotating around the center of the rectangle, is also rotating around a point at it's own border.
I rotate the rectangle with a RenderTransform object, which works well enough and is easy enough.
For the line and the circle, I wrote a method to calculate the rotation.
The line I can calculate without using the angle.
Here's the method:
private void SetPositionOfRotationShaft(Point center)
{
double l = Math.Sqrt(Math.Pow((this.ConnectionLineDirection.X - center.X), 2) + Math.Pow((this.ConnectionLineDirection.Y - center.Y), 2));
double factor = Math.PI / 180;
this.connectionLine.X1 = center.X + (this.surroundingRectangle.Height / (2 * l)) * (this.ConnectionLineDirection.X - center.X);
this.connectionLine.Y1 = center.Y + (this.surroundingRectangle.Height / (2 * l)) * (this.ConnectionLineDirection.Y - center.Y);
this.connectionLine.X2 = center.X + ((this.surroundingRectangle.Height + 40) / (2 * l)) * (this.ConnectionLineDirection.X - center.X);
this.connectionLine.Y2 = center.Y + ((this.surroundingRectangle.Height + 40) / (2 * l)) * (this.ConnectionLineDirection.Y - center.Y);
double translatedLeft = Canvas.GetLeft(this.rotationSign) - center.X;
double translatedTop = Canvas.GetTop(this.rotationSign) - center.Y;
double left = ((translatedLeft * Math.Cos(-this.rotateSurroundingRectangle.Angle*factor)) + (translatedTop * Math.Sin(-this.rotateSurroundingRectangle.Angle*factor))) + center.X;
double top = ((translatedTop * Math.Cos(-this.rotateSurroundingRectangle.Angle * factor)) - (translatedLeft * Math.Sin(-1 * this.rotateSurroundingRectangle.Angle * factor))) + center.Y;
Canvas.SetLeft(this.rotationSign, left);
Canvas.SetTop(this.rotationSign, top);
}
Also curious, when i use the same calculation for the line as i do for the circle, the line rotates at a higher speed. The same thing happend to the circle until i added the factor.
So, the problem was, that i had to set the position of the circle with Canvas.SetLeft() and SetTop(), which is essentialy the upper left corner of a square around the circle.
For my rotation to work, i should have set the center (but that's not possible). So i had to subtract the radius of the circle from top and left.
Canvas.SetLeft(this.rotationSign, left-radius);
Canvas.SetTop(this.rotationSign, top-radius);

UWP: Calculate Transformation based on ScrollViewer

I have a windows universal app where I am rendering a scene with DirectX. I want to make use of the Scrollviewer and therefore I render my scene behind the Scrollviewer and want to calculate the scene transformation based on the Scrollviewer. It works fine so far, especially the translation and scrolling. But when I zoom in, the scene jumps around in two special situations:
The scene had enough space and was centered and now scrolling is required.
The opposite direction.
More or less I use the following code:
float zoom = scrollViewer.ZoomFactor;
float inverseZoom = 1f / scrollViewer.ZoomFactor;
float scaledContentW = Document.Size.X * scrollViewer.ZoomFactor;
float scaledContentH = Document.Size.Y * scrollViewer.ZoomFactor;
float translateX;
float translateY;
if (scaledContentW < scrollViewer.ViewportWidth)
{
translateX = ((float)scrollViewer.ViewportWidth * inverseZoom - Document.Size.X) * 0.5f;
}
else
{
translateX = -inverseZoom * (float)scrollViewer.HorizontalOffset;
}
if (scaledContentH < scrollViewer.ViewportHeight)
{
translateY = ((float)scrollViewer.ViewportHeight * inverseZoom - Document.Size.Y) * 0.5f;
}
else
{
translateY = -inverseZoom * (float)scrollViewer.VerticalOffset;
}
float visibleX = inverseZoom * (float)scrollViewer.HorizontalOffset;
float visibleY = inverseZoom * (float)scrollViewer.VerticalOffset; ;
float visibleW = Math.Min(Document.Size.X, inverseZoom * (float)scrollViewer.ViewportWidth);
float visibleH = Math.Min(Document.Size.Y, inverseZoom * (float)scrollViewer.ViewportHeight);
Rect2 visibleRect = new Rect2(visibleX, visibleY, visibleW, visibleH);
transform =
Matrix3x2.CreateTranslation(
translateX,
translateY) *
Matrix3x2.CreateScale(zoom);
You can get an example here: https://github.com/SebastianStehle/Win2DZoomTest
To be sure that my eyes are not broken I was zooming around and have written the translation and zoom values to a file. You can see it here:
https://www.dropbox.com/s/9ak6ohg4zb1mnxa/Test.png?dl=0
The meaning of the columns is the following:
Column 1: The computed zoom value of the transformation matrix (M11) = ScrollViewer.ZoomFactor
Column 2: The computed x offset of the matrix (See above)
Column 3: The x value of the result of matrix * vector (500, 500), here: Colum1 * 500 + Column2
You see, that the matrix values look good, but when applying the transformation you get this little jump to the right for some milliseconds. One theory was, that the viewport might change because the scrollbar becomes visible. But this is not the case. I also tried fixed values here, made the scrollbars visible and even created a custom template for the scrollviewer with no scrollbars at all.
Btw: This is a cross post, I also asked the question here: https://github.com/Microsoft/Win2D/issues/125
You see this behavior because when you zoom bigger than the ScrollViewer's size, the zoom center point is moved. To fix this, you just need to subscribe to the ScrollViewer's LayoutUpdated event and inside the handler, manually keep its content in the center.
private void ScrollViewer_LayoutUpdated(object sender, object e)
{
this.ScrollViewer.ChangeView(this.ScrollViewer.ScrollableWidth / 2, this.ScrollViewer.ScrollableHeight / 2, this.ScrollViewer.ZoomFactor, true);
}
This should fix the jumpy movement on the two drawed Rectangles from Win2D.
Update
Just to prove my point, the jumpy behavior is most likely due to unusual translate x and/or y value change when the content size goes over the size of the ScrollViewer. So I wrote some code to log these values on the screen as shown below -
...
this.Test1.Text += ((float)translateX).ToString() + " ";
this.Test2.Text += ((float)translateY).ToString() + " ";
session.Transform =
Matrix3x2.CreateTranslation(
(float)translateX,
(float)translateY) *
Matrix3x2.CreateScale((float)zoom);
Now look at the numbers on the image above. What I did was I tried zooming in until the jumpy scene occurred. See the highlighted translate y value? It is slightly greater than its previous value, which is against the declining trend.
So to fix this, you will need to be able to skip these unusual values caused by ScrollViewer.

Relationship between projected and unprojected Z-Values in Direct3D

I've been trying to figure this relationship out but I can't, maybe I'm just not searching for the right thing. If I project a world-space coordinate to clip space using Vector3.Project, the X and Y coordinates make sense but I can't figure out how it's computing the Z (0..1) coordinate. For instance, if my nearplane is 1 and farplane is 1000, I project a Vector3 of (0,0,500) (camera center, 50% of distance to far plane) to screen space I get (1050, 500, .9994785)
The resulting X and Y coordinates make perfect sense but I don't understand where it's getting the resulting Z-value.
I need this because I'm actually trying to UNPROJECT screen-space coordinates and I need to be able to pick a Z-value to tell it the distance from the camera I want the world-space coordinate to be, but I don't understand the relationship between clip space Z (0-1) and world-space Z (nearplane-farplane).
In case this helps, my transformation matrices are:
World = Matrix.Identity;
//basically centered at 0,0,0 looking into the screen
View = Matrix.LookAtLH(
new Vector3(0,0,0), //camera position
new Vector3(0,0,1), //look target
new Vector3(0,1,0)); //up vector
Projection = Matrix.PerspectiveFovLH(
(float)(Math.PI / 4), //FieldOfViewY
1.6f, // AspectRatio
1, //NearPlane
1000); //FarPlane
Standard perspective projection creates a reciprocal relationship between the scene depth and the depth buffer value, not a linear one. This causes a higher percentage of buffer precision to be applied to objects closer to the near plane than those closer to the far plane, which is typically desired. As for the actual math, here's the breakdown:
The bottom-right 2x2 elements (corresponding to z and w) of the projection matrix are:
[far / (far - near) ] [1]
[-far * near / (far - near)] [0]
This means that after multiplying, z' = z * far / (far - near) - far * near / (far - near) and w' = z. After this step, there is the perspective divide, z'' = z' / w'.
In your specific case, the math works out to the value you got:
z = 500
z' = z * 1000 / (1000 - 999) - 1000 / (1000 - 999) = 499.499499499...
w' = z = 500
z'' = z' / w' = 0.998998998...
To recover the original depth, simply reverse the operations:
z = (far / (far - near)) / ((far / (far - near)) - z'')

Updating sprite's alpha as Scale increases

I have a sprite which size changes from 0.6f to 2.0f during an animation.
At the same time I want its alpha value to change from 1.0f to 0 as its scale increase. So the sprite is fully opaque at 0.6f and gradually disappear until the size is 2.0f where it becomes totally transparent.
How do I correlate those values?
spriteBatch.Draw(texture, Vector2.Zero, Color.White*alpha,null,0f,origin,scale,1f);
Where
alpha = MathHelper.Clamp(0, 1 , 1 - (scale - startScale)/deltaScale);
In your case
startScale = 0.6f,deltaScale = 2.0f-0.6f = 1.4f;
It's going to be easier and more expandable if you animate each value separately. In this case scale and alpha, but the same logic could be applied to anything.
Animations are a function of time, so the variable you're missing here is how long you want the animation to last.
Each animation has a startValue and an endValue. From that you can derive 'change per second' like this:
var changePerSecond = (endValue - startValue) / totalSeconds;
Then in each frame, you simply increment the desired property by multiplying by the frame time (deltaTime), e.g.
scale += changePerSecond * deltaTime;
Okay, with the basic concept out of the way you can see how it would work for scale and alpha in your animation (using a 1 second animation as an example).
var scalePerSecond = (2.0f - 0.6f) / 1.0f;
var alphaPerSecond = (0.0f - 1.0f) / 1.0f;
Then in your update method..
scale += scalePerSecond * deltaTime;
alpha += alphaPerSecond * deltaTime;
Note that this is an example of the simplest linear animation off the top of my head to demonstrate the concept. You'll likely need other code to detect the end of the animation and what to do when it finishes.

Move a 2D sprite in a curved upward arc in XNA?

Alright, so here's my problem.
I've been trying to create a sort of a visual day/night cycle in XNA, where I have an underlying class that updates and holds time and a Sky class which outputs a background based on the time that the class updates.
What I can't figure out though is how to make the moon/sun move in a curved upward arc that spans the screen based on what time of the day it is. The most problematic part is getting the Y axis to curve while the X axis moves as the time progresses.
Anyone that could help me here?
EDIT:
Alright, looks like Andrew Russels example helped me to do what I needed to do.
Although I had to expermient for a bit, I finally reached a suitable solution:
float Time = (float)Main.inGameTime.Seconds / (InGameTime.MaxGameHours * 60 * 60 / 2);
this.Position.X = Time * (Main.Viewport.X + Texture.Width * 2) - Texture.Width;
this.Position.Y = Main.Viewport.Y - (Main.Viewport.Y * (float)Math.Sin(Time * MathHelper.Pi) / 2) - (Main.Viewport.Y / 2) + 50;
Try looking at the Math.Sin or Math.Cos functions. These are the trigonometric functions you're looking for.
Something like this (giving a position for SpriteBatch):
float width = GraphicsDevice.Viewport.Width;
float height = GraphicsDevice.Viewport.Height;
float time = 0.5f; // assuming 0 to 1 is one day
Vector2 sunPosition = new Vector2(time * width,
height - height * (float)Math.Sin(time * width / MathHelper.TwoPi));
(Disclaimer: I haven't tested this code.)
There is also the Curve class.

Categories

Resources