1
\$\begingroup\$

I'm trying to build small UI framework. And I want it to have a coordinate space such that origin is placed in top left corner (x increases to right, y increases to down). I think for UI it should be better and easier to work with (but still I have some concerns about this decision). But how to accomplish this right?

What I've tried:

  • I made class Box that creates a mesh like this:
Vector2[] uv = new Vector2[4]
{
    new Vector2(0, 1),
    new Vector2(1, 1),
    new Vector2(0, 0),
    new Vector2(1, 0)
};

Vector3[] vertices = new Vector3[4]
{
    // 0 --- 1
    // |  /  |
    // 2 --- 3
    new Vector3(0, 0, 0),
    new Vector3(width, 0, 0),
    new Vector3(0, height, 0),
    new Vector3(width, height, 0),
};
mesh.vertices = vertices;

int[] triangles = new int[6]
{
    0, 1, 2,
    1, 3, 2
};
mesh.triangles = triangles;

Vector3[] normals = new Vector3[4]
{
    -Vector3.forward,
    -Vector3.forward,
    -Vector3.forward,
    -Vector3.forward
};

then for such box I'm creating MeshRenderer object. Then I apply box position to MeshRenderer.transform. And then to be able to render it not fliped I change camera matrix:

Matrix4x4 translate = Matrix4x4.Translate(new Vector3(screenWidth * .5f, screenHeight * .5f));
Matrix4x4 scale = Matrix4x4.Scale(new Vector3(1, -1, 1));
_camera.worldToCameraMatrix = (_worldToCameraMatrix * translate * scale).inverse;

And it looks like it works. But probably there are some issues with this (I'm not aware about all of them right now), but I've already faced with one: when you look at your mesh renderer in Scene view it's flipped vertically. That's a behavior that I don't want. I need it to look at it in Game view.

Also, I've tried doing something like this:

Vector3[] vertices = new Vector3[4]
{
    new Vector3(tx, ty, 0),
    new Vector3(tx + width, ty, 0),
    new Vector3(tx, ty + height, 0),
    new Vector3(tx + width, ty + height, 0),
};

and it also works but it feels wrong that we need to change mesh when just position changed. Also what if someone need to render one mesh several times at different positions?

  • The other thing I've tried is Graphics.DrawMesh instead of MeshRenderer. This is better because I can specify matrix for drawing. I haven't spent to much with this but probably this one also has some drawbacks. The other reason not to use is that all UI libraries that I checked (like nGUI, Fairly GUI, and other) uses MeshRenderers for drawing. So probably there is a reason for that.
  • The other thing is to create a game object container for MeshRenderers and change its transform accordingly, I haven't tried it, but it seems everything will be flipped in Scene view either;

So, my questions:

  • How to properly define custom coordinate space? What options do I have? What issues I may face with?
  • The other concern is that I'm going to use standard sprites but they geometry are in unity space (y increases up) that means I will need to flipped it somehow to be able to use it? The same may goes with uvs and so on. Is there a way to avoid this complexity somehow? The same issue may be with text?
  • Should I go with top left coordinate space? Or maybe there is no reason for this? Some complexities or it's OK? And shouldn't be that difficult? How will you do this?

I've tried to google and search SO and SE but didn’t find anything. So, any advice very appreciated.

Basically I want to be able to write code like this:

var stage = new Stage();
var box = new Box();
box.width = 100;
box.height = 120;
box.color = Colors.red;
box.x = 0;
box.y = 0;
stage.addChild(box);

And if I run the code I should see my red box in top left corner of the screen. That's it.

Basically I need just the same as here but for Unity. Is this called a change of basis? (I'm not sure). I've looked how it's done in LibGDX, and it looks like not only y is mirrored but z is also. Besides that texture coordinates are also mirrored, but I don't see any changes to meshes (or didn't find). How can I flip uv coordinates? In unity they are not always a rectangle, so I think, I can't just flip them somehow as it's done in LibGDX. It seems (from what I've tested) mesh vertices also should be flipped. How to this?

So how can I accomplish this?

Upd:

This is my progress so far:

using System.Linq;
using System.Text;
using UnityEngine;

public class BoxRenderer : MonoBehaviour
{
    public float width = 1;
    public float height = 1;

    public bool useMeshRenderer = true;
    public bool useSpriteGeometry = true;

    public Sprite sprite;

    public Vector3 position;

    [SerializeField] 
    private Mesh _mesh;
    private Matrix4x4 _matrix;
    private Material _material;

    private MeshFilter _meshFilter;
    private MeshRenderer _meshRenderer;

    private void Awake()
    {
        _mesh = new Mesh();
        _material = new Material(Shader.Find("Sprites/Default"));

        UpdateState();
    }

    private void OnValidate()
    {
        if (!Application.isPlaying || _mesh == null || !enabled)
            return;

        UpdateState();
    }

    private void UpdateState()
    {
        Mesh mesh = _mesh;
        mesh.Clear();
        
        // 0 --- 1
        // |  /  |
        // 2 --- 3

        Vector3[] vertices = new Vector3[4]
        {
            new Vector3(0, 0, 0),
            new Vector3(width, 0, 0),
            new Vector3(0, height, 0),
            new Vector3(width, height, 0)
        };

        int[] triangles = new int[6]
        {
            0, 1, 2,
            1, 3, 2,
        };

        Vector3[] normals = new Vector3[4]
        {
            -Vector3.forward,
            -Vector3.forward,
            -Vector3.forward,
            -Vector3.forward
        };

        Vector2[] uv = new Vector2[4] {new(0, 0), new(1, 0), new(0, 1), new(1, 1)};

        if (sprite != null && (sprite.uv.Length == 4 || useSpriteGeometry))
        {
            uv = sprite.uv;
            _material.mainTexture = sprite.texture;
        }

        if (sprite != null && useSpriteGeometry)
        {
            vertices = sprite.vertices.Select(v => (Vector3) v).ToArray();
            triangles = sprite.triangles.Select(t => (int) t).ToArray();
            normals = sprite.vertices.Select(_ => -Vector3.forward).ToArray();

            Vector2 pivot = sprite.pivot / sprite.pixelsPerUnit;
            float spriteHeight = sprite.rect.height / sprite.pixelsPerUnit;

            for (int i = 0; i < vertices.Length; ++i)
            {
                vertices[i].x += pivot.x;
                vertices[i].y = spriteHeight - (vertices[i].y + pivot.y);
            }
        }

        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.normals = normals;
        mesh.uv = uv;

        Camera camera = Camera.main;

        Vector3 min = camera.ViewportToWorldPoint(Vector3.zero);
        Vector3 max = camera.ViewportToWorldPoint(Vector3.one);

        float screenWidth = Mathf.Abs(max.x - min.x);
        float screenHeight = Mathf.Abs(max.y - min.y);

        Vector3 s = new Vector3(1, -1, 1);
        Vector3 t = new Vector3(-screenWidth * .5f, screenHeight * .5f);
        Matrix4x4 scale = Matrix4x4.Scale(s);
        Matrix4x4 translate = Matrix4x4.Translate(t);
        _matrix = translate * scale;

        if (useMeshRenderer)
        {
            if (_meshFilter == null)
            {
                _meshFilter = gameObject.AddComponent<MeshFilter>();
                _meshRenderer = gameObject.AddComponent<MeshRenderer>();
                _meshRenderer.transform.localScale = new Vector3(1, -1, 1);
            }
        }
        else if (_meshFilter != null)
        {
            Destroy(_meshFilter);
            Destroy(_meshRenderer);
            _meshFilter = null;
            _meshRenderer = null;
        }
    }

    private void Update()
    {
        if (useMeshRenderer)
        {
            _meshFilter.mesh = _mesh;
            _meshRenderer.sharedMaterial = _material;
            transform.position = _matrix.MultiplyPoint(position);
        }
        else
        {
            Matrix4x4 m = _matrix * Matrix4x4.Translate(position);
            Graphics.DrawMesh(_mesh, m, _material, 0);
        }
    }
}

It kind of works but... DrawMesh part looks much better, but MeshRenderer part doesn't feel good. Also it looks like I will have issues with other transformations. I will need to extract that info from matrix? This doesn't feel good. It's probably should be a better solution.

\$\endgroup\$
6
  • \$\begingroup\$ If you don't need physics it should be possible to just change the amount of output in the shader pipeline, maybe in vertex or geometry or domain or hull shader (don't remember which one could create many meshes from just one, but I know I tried it before). \$\endgroup\$
    – Emil
    Mar 19, 2022 at 6:44
  • \$\begingroup\$ If you don't care about framerate you could probably have a helper class that maps back and forth between unity space and "your" space. \$\endgroup\$
    – Emil
    Mar 19, 2022 at 7:04
  • \$\begingroup\$ What do you mean with wanting a different space, is it in the presentation you want this to happen? Like bigger x coordinate should make the object move right on the screen? \$\endgroup\$
    – Emil
    Mar 19, 2022 at 7:09
  • \$\begingroup\$ I've added an example of what I want to achieve. Hope it's more clear now. \$\endgroup\$
    – ChessMax
    Mar 19, 2022 at 8:08
  • \$\begingroup\$ Your example looks like a MonoBehavior? I think it would be easier to do the coordinate transformation in the shader? Unless you have text - that will probably be messed up. \$\endgroup\$
    – Emil
    Mar 24, 2022 at 6:37

1 Answer 1

1
\$\begingroup\$
public Canvas myCanvas;
transform.position = mousePos * myCanvas.scaleFactor;

This is how I designed some UI elements. Basically this grabs my mousePos in screen space and makes it proportional to the current canvas scale. If you are making a UI framework, you should consider other screens. The canvas scales with the screen size, if set up properly. You should be able to do box.width = 100 * myCanvas.scaleFactor. stage.Add just needs to put the game object on the canvas, and give it a reference.

This was a huge pain to find out myself. I dug through docs and google for an entire day as well. This was a self made solution, and the only one that I could find.

As far as designing your vertices, I think that's a rabbit hole not worth chasing. You can just move the anchor to where you need it on the box, and move it into position. All it would need is coordinates and a size.

\$\endgroup\$
3
  • \$\begingroup\$ I'm not going to use Canvas. The question is how to implement my own canvas but with different coordinate space. \$\endgroup\$
    – ChessMax
    Mar 23, 2022 at 7:45
  • \$\begingroup\$ Just curious, why reinvent the wheel? Is there a limitation on the canvas you are trying to evade? Why not extend its behavior to your needs? \$\endgroup\$
    – Mike
    Mar 23, 2022 at 14:18
  • \$\begingroup\$ I want to understand how some things work internally. Gain some experience. Want to make a toolkit that will be easy to use for my own needs. \$\endgroup\$
    – ChessMax
    Mar 23, 2022 at 17:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .