7 minutes
Using Unity low-level graphics APIs to draw custom charts
Recently, while designing some Unity Editor extensions for easily creating decisional fuzzy systems, I thought it could have been great to provide users a visual representation of the fuzzy sets they are designing. If you have no idea what I am talking about, just focus on this problem: we want to draw custom charts on a Unity custom inspector. In fact, in this post I am going to show how it is possible to use the Unity low-level graphics APIs to draw everything.
If you are not interested in the process and you just want to draw some charts in your game textures or custom inspectors, you can directly skip to the end of this post.
What’s wrong with AnimationCurve?
If you are familiar with animation design in Unity, you may have already thought about AnimationCurve and maybe you have already made experiments with it. Unity animation curves are pretty cool, but they do not give you the kind of control I like to have. Moreover, they have some limitations given by the fact that they have been designed for animation editing and not for general chart plotting.
Firstly, you cannot draw AnimationCurves in a scene, e.g., on a texture. Secondly, there not seems to be a way to draw multiple curves in the same chart. Finally, if you want to see the complete curve in a reasonable resolution, you need to put your AnimationCurve in a CurveField, click on it and watch your curve in a separate window, which breaks the flow in some way.
I started digging into low-level graphics APIs after having read this code sample from Bunny83 on UnityAnswers. This whole post and the tool I created come from that code sample, so thank you Bunny83. What I did was simply wrap his code into a general-purpose tool for plotting custom functions in the inspector or on a texture. So, let’s start digging into GL APIs.
Unity GL APIs
As the name suggests, Unity GL class allows to call OpenGL directives from a C# script. Basically, it allows you to setup an OpenGL scene in any part of the screen. What you need to do is defining a Rect inside which the OpenGL scene will be rendered. You will need a shader to invoke for the rendering pass: in this tutorial, we will use a Unity hidden shader, named “Hidden/Internal-Colored” for simply rendering geometric shapes with a full color, but you can use any shader you want.
After that, you can start placing vertices all around the scene. Here is an exmaple of the setup environment:
The way vertices will be interpreted and rendered depends on the environment you called before starting to add them. For example, you can draw quads by placing vertices inside a GL.QUADS environment.
Another very useful OpenGL environment provided by the GL APIs is GL.LINES, which will play an important role in our chart plotting tool. Just a note before starting playing with it: GL.LINES will draw on line for each pair of GL.Vertex3 calls inside its environment. This means that, if you want to draw a line that goes from point A to point B and from point B to point C, you will need to call GL.Vertex3 four times, because you need to tell GL to draw two straight lines with a shared vertex.
But let’s focus on the parameters I gave in GL.Vertex3 calls. How are the vertex coordinates (10, 10, 0) interpreted? Well, if you leave the code as it is, the vertex will be drawed 10 pixels right and 10 pixels below the origin, as the GL reference frame is a top-left, which means that the X coordinate increases from left to right and the Y coordinate increases from up to down. This is not very comfortable when plotting functions, since most of the times we will need to work in a bottom-left origin reference frame, where the Y increases from bottom to the top. Moreover, we may find too strict the constraint that forces us to draw in pixel space, when we would prefer, for example, to plot a function in the (-1, +1) interval on both the X and the Y and we would like to express our coordinates with floats ranging in that interval. So let’s add some processing on the data we provide as vertex coordinates: firstly, we define two Rects: one describes the draw space in a scale which is comfortable to the user, for example (-1, +1) on the X axis and (-0.5, 1) on the Y axis; the second one describes the draw space in pixel size, for example (10, 110) along the X axis and (10, 60) along the Y axis. After that, we can convert each pair of coordinates in user space to a new pair of coordinates in pixel space, that will be passed to GL calls.
If the code above looks a bit obscure, here’s what it does:
- GL renders only vertices with positive coordinates, so I shift them. Note that at this point coordinates are still expressed in user space.
- Here I convert coordinates from user space to pixel space with a simple computation of the rects width and height ratios.
- Finally I convert the coordinates from top-left to bottom-left origin. The X is the same, while the Y is reversed.
It is also possible to draw images on the chart. All we need to do is draw a quad and set its UV coordinates for rendering a texture on it. Note that to do this the “Hidden/Internal-Colored” shader cannot be used: we can switch to the “Hidden/Internal-GUITexture” one.
Here’s an example of what you can do with the utilities I described above. Note that the numerical labels are textures representing digits that I drawed with the above code: font rendering through GL calls is complex and would require an entire post. Maybe I will explore that in future.
Exporting to Texture2D
It is possible to render a GL scene (including our charts) to a Texture2D which we can use in our scenes by using a temporary RenderTexture. The following code comes from this Unity forum post and, even if it has been written for Unity 4, it is still valid.
Here is an example of the result. The main problem of this texture is that blurred effect, but I think you can improve the graphic quality with an appropriate shader and/or a better study on the output texture resolution.
A tool for drawing charts
I made a tool that provides a set of APIs for drawing everything you saw on this post, without needing to mess with the GL methods. You can get it from my github.
As I always say, there is not an exact solution to any problem. I do not expect to give the exact solution, so, if you think to know a better way to do this or you find something wrong in my code, feel free to leave a comment below and discuss.
Hope this post will be helpful, see you the next time!
1445 Words
2019-04-02 13:00
Did you like this post? Did you hate it? Wanna talk about it? Contact me at alessandro.tironi7 at gmail dot com.