Search code examples
c#unity-game-enginemathunity-editorunity-ui

How to scale Matrix4x4 around point?


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?*/;
}

Solution

  • 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.