Jump to content

2D Drawing in Leadwerks 5 beta

Josh

1,418 views

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 3


11 Comments


Recommended Comments

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

Share this comment


Link to comment

Here it is working. The text always draws on top of the scene with the blue rectangle under it.

Image15.thumb.jpg.8fe3054691a62e6badf347da110437eb.jpg

  • Like 1

Share this comment


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?

Share this comment


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

Share this comment


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

Share this comment


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

Share this comment


Link to comment

For some reason this blog has three times as many views as most of my other articles.

  • Like 1
  • Haha 1

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

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.

  • Blog Entries

    • By Josh in Josh's Dev Blog 3
      What's new
      EAX audio effects for supported hardware. Source class renamed to "Speaker". Plane joint for 2D physics, so now you can make Angry Birds with Vulkan graphics. Fixed DPI issues with fullscreen mode. Added impact noise to barrels, fixed Lua collision function not being called. Script functions now start with "Entity:" instead of "Script:", i.e. Entity:Update() instead of Script:Update(). Additionally, four examples can be run showing various functionality. Double-click on the .bat files to launch a different demo:
      First-person shooter game. 2D physics demonstration. Advanced 2D drawing with text, rotation, and scaling. Multi-camera setup.
    • By 💎Yue💎 in The shock absorbers 0
      It's interesting that when you become an expert on something, you're not sparing any effort to see how something works, but rather you're focusing on creating something. And so everything becomes easier.
      At this point of learning there is a glimpse of a low idea of creating a game, but the secret of all this is to keep it simple and to be very clear that a game is a game, and not an exact simulation of the real world. For example anyone who has a low idea of the red planet, will understand no matter the colors of the scene that is a terrain of Mars, even if it is not very real what is transmitted, a game, that's just it.
      At this point I already have an astronaut character who runs from one place to another on a very large 4096 x 4046 terrain that would surely take a long walk. My previous prototype projects involve a vehicle, but I didn't get the best implementation prospect in that time and I always found performance problems in my machine, something that isn't happening with the character controller for a third person player. 
      As always, I think I'm a scavenger looking for game resources, that's where this community exposes links to websites with interesting hd textures, and one or another model searched on the net, but what I've greatly improved is learning to write code, I have a better workflow, writing Lua code focused on the paradigm of object programming.



      Something interesting is the system of putting rocks, all very nice from the point of implementing them. And it works very well with the character controller if you put collision in cube form.
      I've been thinking about implementing a car system, I think it would be necessary in such a large terrain, but I think it's not the time, my previous experience, involves deterioration in performance and something I think is the physics of the car with respect to the terrain and rocks that in the previous project involve deterioration in the fps. Although if you implement a car would have an option would be to remove the rocks, but I prefer not to have a car and if you have rocks. 
       
       
       
       
    • By reepblue in reepblue's Blog 6
      Loading sounds in Leadwerks has always been straight forward. A sound file is loaded from the disk, and with the Source class emits the sound in 3D space. The sound entity also has a play function, but it's only really good for UI sounds. There is also Entity::EmitSound() which will play the sound at the entity's location. (You can also throw in a Source, but it'll auto release the object when it's done.)
      While this is OK for small games, larger games in which sounds may change might mean you have to open your class, and adjust the sounds accordingly. What if you use the sound in multiple places and you're happy with the volume and pitch settings from an earlier implementation? You could just redefine the source in a different actor, but why should you?
      A solution I came up with comes from SoundScripts from the Source Engine. With that engine, you had to define each sound as a SoundScript entry. This allowed you to define a sound once, and it allowed for other sound settings such as multiple sounds per entry. I thought this over, and with JSON, we can easily create a similar system for Leadwerks 4 and the new engine.
      I first started with a dummy script so I can figure out how I wanted the end result to be.
      { "soundData": { "Error": { "file": "Sound/error.wav", "volume": 1.0, "pitch": 1.0, "range": 0.25 }, "RandomSound": { "files": { "file1": "Sound/Test/tone1.wav", "file2": "Sound/Test/tone2.wav", "file3": "Sound/Test/tone3.wav" }, "volume": 1.0, "pitch": 1.0, "range": 0.25 } } } In this script, we have two sound entries. We have an error sound (Which is suppose to be the fall back sound for an invalid sound entry) and we have a sound entry that holds multiple files. We want a simple, straight forward. entry like "Error" to work, while also supporting something "RandomSound" which can be used for something like footstep sounds.
      The script is streamed and stored into multiple structs in a std::map at the application start. We use the key for the name, and the value is the struct.
      typedef struct { std::string files[128]; char filecount; float volume; float pitch; float range; bool loopmode; } sounddata_t; std::map<std::string, sounddata_t> scriptedsounds; Also notice that we don't store any pointers, just information. To do the next bit, I decided to derive off of the engine's Source class and call it "Speaker". The Speaker class allows us to load sounds via the script entry, and support multiple sounds.
      You create one like this, and you have all the functionalities with the Source as before, but a few differences.
      // Speaker: auto speaker = CreateSpeaker("RandomSound"); When you use Play() with the speaker class and if the sound entry has a "files" table array, it'll pick a sound at random. You can also use PlayIndex() to play the sound entry in the array. I also added a SetSourceEntity() function which will create a pivot, parent to the target entity. From there, the Play function will always play from the pivot's position. This is a good alternative to Entity::EmitSound(), as you don't need to Copy/Instance the Source before calling the function as that function releases the Source as mentioned earlier. Just play the speaker, and you'll be fine! You can also change the sound entry at anytime by calling SetSoundEntry(const std::string pSoundEntryName); The creation of the Speaker class will start the JSON phrasing. If it has already been done, it will not do it again.
      Having sounds being loaded and stored like this opens up a lot of possibles. One thing I plan on implementing is a volume modifier which will adjust the volume based on the games volume setting.Right now, it uses the defined volume setting. It's also a part of another system I have in the works.
×
×
  • Create New...