Sparrow WebGL Devlog 19: 2D Updates
Completely redoing all 2D elements of my WebGL engine
24.04.2024 - 17:12When I started working on my Sparrow WebGL engine, the main goal was to develop simpler 2D games. Here is a quote from the first devlog: “I’m probably going to focus mostly on 2D features in the beginning, because the game I want to work on is 2D.”
However, if you look at my recent blog posts, you’ll notice that all of them are exclusively about 3D features. The last 2D-focused devlog is #3 almost a year ago.
The shift in priorities happened last summer when I had the idea to create a 3D portfolio website with WebGL and naturally, I also wanted to do it with my own engine.
Like every project known to men, creating the portfolio website took a lot longer than originally intended. Although the main slowdown wasn’t developing a WebGL engine from scratch; it was writing the texts.
Two weeks ago, I finally finished the first version of the website and while waiting for private feedback before making it public, I took the opportunity and got back to working on 2D engine features.
2D Objects
While the existing 2D features were good enough to create 2D games (I made a Ludum Dare game with them), I wanted to make them more engine-like. The two most important additions were transformation matrices and parent-child relationships.
Every 2D object has a translation, rotation, and scale, which are multiplied together to create the model matrix. You can also add a 2D object as a child of another 2D element, in which case the transformation matrix of the parent affects the child too. This is exactly how it works in 3D.
To achieve this, I created a new Object2D superclass for all 2D objects similar to the existing Object3D class. Since the 2D and 3D versions of the class shared a lot of features, I combined them into a general Object superclass for both of them. I also redid the Mesh2D class to follow the design of the Mesh3D version.
With these changes 2D and 3D objects are now very similar in their structure, which is good for the overall coherence and consistency of the engine.
Parent-child relationships of 2D objects.
Object Origins
Using the new Object2D class I was able to update the quad and text classes, which are the most important 2D objects and if you want to be precise, texts are just a quad with a text texture too. However, the updated classes had a problem with the transformation matrices.
When creating a 2D mesh, you have to make a decision about its origin. You can either put the origin in the center or at the top left corner (or any other corner or position if you want to be freaky). Previously, the objects had their origins at the top left corner because that’s the most common solution. However, when you rotate an object, it rotates around its origin, and rotating around the top left isn’t what most people would expect as normal behavior.
Rotating around the center makes much more sense. My first idea was to create two new classes: CenterQuads and CenterTexts before I realized that adding an origin option to the normal Quad and Text objects would be a better solution. Now you can create objects with different origins depending on your use case.
2D Object Picking and Outlines
Adding transformation matrices introduced another problem for 2D objects. Normally, checking whether a 2D object is clicked or hovered is trivially easy by checking the x and y coordinates individually. However, as soon as you rotate the object, this doesn’t work anymore.
There are a few ways you can solve this, but since I had already implemented color picking for 3D objects, I implemented it for 2D objects too. Check the blog post for more details, but here is a quick summary: color picking works by rendering all interactable objects to a texture with a unique color and then sampling the texture at the mouse position to figure out which object was clicked.
I created a separate version for 2D color picking, but when I tried to use 2D and 3D color picking at the same time, the application lagged a lot. I quickly realized my mistake. I was using gl.readPixels() in both versions and gl.readPixels() is notoriously slow because it's a blocking function.
Realistically, most applications would use either 2D or 3D color picking and not both of them at the same time, so it probably would have been fine, but it felt like an unsatisfying solution anyway. Instead, I combined both of them into a single class that could render 2D and 3D objects to the same texture, so that gl.readPixels() only has to be called once per frame.
Finally, I also updated outline rendering and extended it to 2D objects. This also required adding new shaders for 2D objects but the concept of edge detecting a depth texture stayed the same.
Hovering the Hello World quad shows the outline. You can also see the color picking and outline debug textures.
2D Particles
The last 2D feature that required an update was particles. The previously implemented mechanics were still fine, but I wanted to change the particle manager and options menu to behave like their 3D counterparts from the last devlog. This meant moving the 2D particle manager to the engine and automatically updating and drawing the active emitters.
The options menu was a bigger change. The old 2D options menu was implemented as an HTML overlay, while the new 3D version uses the in-engine UI features. While HTML is also a valid solution, I wanted to make both of them similar. Since most of the settings of 2D and 3D particle emitters are the same, I copied the 3D options menu and removed the third dimension.
The biggest change to the 2D particle emitters was making them inherit from Object2D and therefore turning them into an object that can have parents and children. This makes creating certain assets a lot easier; for example, a campfire can now have fire and smoke particle emitters as children.
However, particles use a custom shader, which means they cannot be rendered in between the other objects (frequent shader changes are bad). I had to change how drawing 2D objects works so that particle emitters in a tree of objects get skipped and rendered later when the particle manager draws all active emitters.
Finally, I added debug shapes for the 2D particle emitters, which make it a lot easier to see the exact shape of the emitter, especially when it is transformed.
Different 2D particle emitters and the options menu.
I’m very happy with the new 2D features. Pretty much all 2D elements of the engine had a massive overhaul and feel much more coherent and consistent. Every 2D object can be transformed in the same way and all of them can be parents and children of each other.
by Christian - 24.04.2024 - 17:12
Comments
Comments are disabled