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 ofMeshRenderer
. 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) usesMeshRenderers
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.