Jump to content

2D Drawing in Leadwerks 5 beta


Josh

6,740 views

 Share

Previously I described how multiple cameras can be combined in the new renderer to create an unlimited depth buffer. That discussion lead into multi-world rendering and 2D drawing. Surprisingly, there is a lot of overlap in these features, and it makes sense to solve all of it at one time.

Old 2D rendering systems are designed around the idea of storing a hierarchy of state changes. The renderer would crawl through the hierarchy and perform commands as it went along, rendering all 2D elements in the order they should appear. It made sense for the design of the first graphics cards, but this style of rendering is really inefficient on modern graphics hardware. Today's hardware works best with batches of objects, using the depth buffer to handle which object appears on top. We don't sort 3D objects back-to-front because it would be monstrously inefficient, so why should 2D graphics be any different?

We can get much better results if we use the same fast rendering techniques we use for 3D graphics and apply it to 2D shapes. After all, the only difference between 3D and 2D rendering is the shape of the camera projection matrix. For this reason, Turbo Engine will use 2D-in-3D rendering for all 2D drawing. You can render a pure 2D scene by setting the camera projection mode to orthographic, or you can create a second orthographic camera and render it on top of your 3D scene. This has two big implications:

  • Performance will be incredibly fast. I predict 100,000 uniquely textured sprites will render pretty much instantaneously. In fact anyone making a 2D PC game who is having trouble with performance will be interested in using Turbo Engine.
  • Advanced 3D effects will be possible that we aren't used to seeing in 2D. For example, lighting works with 2D rendering with no problems, as you can see below. Mixing of 3D and 2D elements will be possible to make inventory systems and other UI items. Particles and other objects can be incorporated into the 2D display.

D_bg6x1XUAAE4_L.png.0b962282c596cfddfd487e5a0ff32d99.png

The big difference you will need to adjust to is there are no 2D drawing commands. Instead you have persistent objects that use the same system as the 3D rendering.

Sprites

The primary 2D element you will work with is the Sprite entity, which works the same as the 3D sprites in Leadwerks 4. Instead of drawing rectangles in the order you want them to appear, you will use the Z position of each entity and let the depth buffer take care of the rest, just like we do with 3D rendering. I also am adding support for animation frames and other features, and these can be used with 2D or 3D rendering.

greenzombie-running-spritesheet.thumb.jpg.8568ef0009033a2b718a3e51293ad293.jpg

Rotation and scaling of sprites is of course trivial. You could even use effects like distance fog! Add a vector joint to each entity to lock the Z axis in the same direction and Newton will transform into a nice 2D physics system.

Camera Setup

By default, with a zoom value of 1.0 an orthographic camera maps so that one meter in the world equals one screen pixel. We can position the camera so that world coordinates match screen coordinates, as shown in the image below.

auto camera = CreateCamera(world);
camera->SetProjectionMode(PROJECTION_ORTHOGRAPHIC);
camera->SetRange(-1,1);
iVec2 screensize = framebuffer->GetSize();
camera->SetPosition(screensize.x * 0.5, -screensize.y * 0.5);

Note that unlike screen coordinates in Leadwerks 4, world coordinates point up in the positive direction.

800px_COLOURBOX18230258.jpg.ec511434e458e48fe6d49ab97b4606c0.jpg

We can create a sprite and reset its center point to the upper left hand corner of the square like so:

auto sprite = CreateSprite(world);
sprite->mesh->Translate(0.5,-0.5,0);
sprite->mesh->Finalize();
sprite->UpdateBounds();

And then we can position the sprite in the upper left-hand corner of the screen and scale it:

sprite->SetColor(1,0,0);
sprite->SetScale(200,50);
sprite->SetPosition(10,-10,0);

2.jpg.29a1c7bcb2b37c3f1e2f94d503a1b876.jpg

This would result in an image something like this, with precise alignment of screen pixels:

Image1.jpg.9ecc9cd52fbb5a1869664cc02b36b8f4.jpg

Here's an idea: Remember the opening sequence in Super Metroid on SNES, when the entire world starts tilting back and forth? You could easily do that just by rotating the camera a bit.

Displaying Text

Instead of drawing text with a command, you will create a text model. This is a series of rectangles of the correct size with their texture coordinates set to display a letter for each rectangle. You can include a line return character in the text, and it will create a block of multiple lines of text in one object. (I may add support for text made out of polygons at a later time, but it's not a priority right now.)

shared_ptr<Model> CreateText(shared_ptr<World> world, shared_ptr<Font> font, const std::wstring& text, const int size)

The resulting model will have a material with the rasterized text applied to it, shown below with alpha blending disabled so you can see the mesh background. Texture coordinates are used to select each letter, so the font only has to be rasterized once for each size it is used at:

Image1.jpg.7ddd9e0759cd1b79e350bfa5f16cbd87.jpg

Every piece of text you display needs to have a model created for it. If you are displaying the framerate or something else that changes frequently, then it makes sense to create a cache of models you use so your game isn't constantly creating new objects. If you wanted, you could modify the vertex colors of a text model to highlight a single word.

Image1.jpg.17b8e38ba53e6d1536261b59d4c4e0bd.jpg

And of course all kinds of spatial transformations are easily achieved.

Image1.jpg.a920c2a3f766193213f9648e3175ed10.jpg

Because the text is just a single textured mesh, it will render very fast. This is a big improvement over the DrawText() command in Leadwerks 4, which performs one draw call for each character.

The font loading command no longer accepts a size. You load the font once and a new image will be rasterized for each text size the engine requests internally:

auto font = LoadFont("arial.ttf");
auto text = CreateText(foreground, font, "Hello, how are you today?", 18);

Combining 2D and 3D

By using two separate worlds we can control which items the 3D camera draws and which item 2D camera draws: (The foreground camera will be rendered on top of the perspective camera, since it is created after it.) We need to use a second camera so that 2D elements are rendered in a second pass with a fresh new depth buffer.

//Create main world and camera
auto world = CreateWorld();
auto camera = CreateCamera(world);
auto scene = LoadScene(world,"start.map");

//Create world for 2D rendering
auto foreground = CreateWorld()
auto fgcam = CreateCamera(foreground);
fgcam->SetProjection(PROJECTION_ORTHOGRAPHIC);
fgcam->SetClearMode(CLEAR_DEPTH);
fgcam->SetRange(-1,1);
auto UI = LoadScene(foreground,"UI.map");

//Combine rendering
world->Combine(foreground);

while (true)
{
	world->Update();
	world->Render(framebuffer);
}

Overall, this will take more work to set up and get started with than the simple 2D drawing in Leadwerks 4, but the performance and additional control you get are well worth it. This whole approach makes so much sense to me, and I think it will lead to some really cool possibilities.

As I have explained elsewhere, performance has replaced ease of use as my primary design goal. I like the results I get with this approach because I feel the design decisions are less subjective.

  • Like 5
 Share

10 Comments


Recommended Comments

The image above is using a perspective projection btw. You can see the lines converge at a point in the distance.

Link to comment

What does the code for differently coloured letters look like? Do you call CreateText once for every letter and then position them manually or is there a nicer way?

Link to comment
14 hours ago, Ma-Shell said:

What does the code for differently coloured letters look like? Do you call CreateText once for every letter and then position them manually or is there a nicer way?

The text is a model, so you can just modify the vertex colors:

	auto text = CreateText(foreground, font, L"Hello, how are you today?\nI am fine.", 72, TEXT_CENTER);
	for (int v = 0; v < text->lods[0]->meshes[0]->vertices.size() / 4; ++v)
	{
		int r = Random(0, 255);
		int g = Random(0, 255);
		int b = Random(0, 255);
		for (int n = 0; n < 4; ++n)
		{
			text->lods[0]->meshes[0]->vertices[v * 4 + n].color[0] = r;
			text->lods[0]->meshes[0]->vertices[v * 4 + n].color[1] = g;
			text->lods[0]->meshes[0]->vertices[v * 4 + n].color[2] = b;
		}
	}
	text->lods[0]->meshes[0]->Finalize();

 

  • Thanks 1
Link to comment

During implementing of Z-sorting for transparency, I found that Vulkan might actually preserve the rasterization order in render passes, which gives me reason to rethink this.

Image1.jpg.3500c8c481a3a99ad565ba3b9075bcc1.jpg

Cameras in Turbo Engine always calculate and send an orthographic matrix to the GPU so it might be better to render 2D primitives simply by switching their shader family to a set of shaders that uses the orthographic matrix instead. In any case, I certainly have learned a lot more about Vulkan in the last couple of days.

  • Like 2
Link to comment

I am working something out that is a little bit more conventional, but retains some of the properties discussed here. Stay tuned...

  • Like 1
Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...