Sparrow WebGL Devlog 2: UI
Adding UI Elements, Processing Inputs, and Minimization25.04.2023 - 22:14
In most cases, native user interfaces for WebGL engines are not needed, as HTML already provides this functionality. You cannot tell the difference between a button that is rendered in WebGL and an HTML <button> element on top of the WebGL canvas. If anything, the WebGL button probably looks worse.
However, I was never a fan of layering different systems on top of each other. Although it's possible to use Qt UI elements and OpenGL together, I have always preferred the minimalistic window management offered by the GLFW library over the more complex Qt system. Additionally, designing and implementing a UI system is challenging and fun.
I didn't have to start from scratch with the UI of my WebGL engine. It's heavily inspired by the one from my OpenGL engine. It works very well and has proven itself in many projects. Another advantage of using a similar system is that switching between them is much easier. However, there are also a few improvements I made in the WebGL version.
The main UI class has a list of elements and handles all higher-level functions of the UI system. However, the elements are also organized in a tree structure. You can add elements directly to the main UI class, but you can also add children to every element. The children functionality is implemented in the superclass, which means it's independent of the individual components. For example, when the main UI class receives a mouse event, it checks all elements which are directly attached to it and if one of them catches the event, the element then checks whether one of its children or itself gets to process the event.
Drawing is handled similarly. The main UI class only calls the draw methods of its direct elements and then every element recursively calls the draw methods of its children. In 2D rendering, triangles are always rendered on top of everything else (if depth testing is disabled), which means children are always drawn on top of their parents. The elements also have a z-index, which can be used to sort them too. Another advantage of the parent calling the draw methods of its children is the scissor test. It's a WebGL function to limit the rendering to a given rectangle. This is very useful to prevent overflows. For example, when scrolling the children in a container, they are just moved according to the scrollbar position. Without the scissor test, they would go outside of the container.
Speaking of scrollbars, they are always the most annoying functionality to implement in a UI system. They have to support many different input methods like the mouse wheel, clicking the up and down buttons, dragging the scrollbar, and clicking into the empty scrollbar area. Then they have to dynamically adjust the children of the scrolled container, which always requires exceptions because the scrollbar itself, which is also a child of the container, must not move itself. It took me a very long time to get scrollbars working properly when I worked on the OpenGL UI library. Luckily, for the WebGL version, I could use my existing solution.
One of the biggest differences between the OpenGL system and my new WebGL implementation is the integration into the engine itself. The OpenGL system was a bit of an afterthought and I only added it later, which meant it wasn't well integrated and always required a lot of extra code when using it in a project. This time around, I worked on the UI system immediately and in parallel to the main WebGL engine, which means it's much better integrated. When creating a new UI in a project, it only requires a single line of code to add the UI to the engine, and from there everything else is handled automatically.
All currently added UI Elements:
Besides the functionality, the UI elements also have to look good. Additionally, it should be possible to change their appearance easily in the code. Just like in my OpenGL UI system, I took some inspiration from CSS. Every element has a style that is initialized with some global default values. Next, every element has a custom style applied based on its type, and finally, you can pass a style argument to the element when creating it. Later values override earlier ones, similar to the CSS hierarchy.
Finally, I also worked on merging and minimizing all engine files. I wasn't sure which tool to use for this, but after a bit of research, I found UglifyJS. It's quite minimalistic, which is the main requirement for me to avoid too many dependencies. In fact, it doesn't have any 3rd party dependencies and the node_modules folder is only ~1MB and 24 files, which is amazing, more node modules should be like this. I wrote a few small scripts that list all files from a directory and convert them to the input format that UglifyJS expects. There may be an option to automatically merge all files from a directory, but for some of my files the inclusion order matters so I had to write a custom script for this anyway. I half expected there to be some weird issues, but I tried my test project with the minimized files instead of the individual ones and everything still worked.
by Christian - 25.04.2023 - 22:14