With the current scale of a game like League of Legends, it can be hard to remember the humble beginnings: a small group of developers too busy shipping a game and putting out fires to think about fine tuning systems, pipelines, and processes. And while we’ve changed a lot, our priorities remain the same: we’ll always put player experience before tech and process. Sometimes that leads to tech debt, and as we grow, it's important that we look for ways to improve the quality of our work as well as the way we work. Not every step forward has to be revolutionary. In this article I want to dig into a series of iterative improvements we made to a system as old as League of Legends itself to ship our latest Ultimate skin: Elementalist Lux.

When engineers supporting the skins team were approached by producers on the Personalization initiative with the idea of Elementalist Lux , a lot had changed since the team released its previous Ultimate Skin, DJ Sona. Our skin development pipeline had matured, we’d finally enforced proper memory budgets for each champion, and we’d learned new lessons with each skin we shipped. We’d come a long way, but the pipeline still wasn’t (and isn’t) where we wanted it to be. So when we began to break the vision for Elementalist Lux down into technical requirements, it was clear that we had some work to do.

The Memory Problem

We work hard to make sure League of Legends runs well whether you’re playing on a beefy gaming rig, an ultrabook, or a less-than-modern computer in a PC cafe, and that means working with limited resources. Textures, polygons, visual effects, sound effects, animation data, and anything else that gives life to a champion takes up precious resources, so we’re constantly making tradeoffs and thinking about new ways to save on space.

The initial conversation about Elementalist Lux’s memory requirements sounded something like this: “Lux will have 10 forms, each the size and scope of a full skin. One full skin takes about 20 megabytes of in-game memory, so we would need 200 megabytes for Elementalist Lux.” With a maximum memory budget of 30 megabytes per skin, this obviously was not going to fly.

We immediately began brainstorming a solution. Our initial investigations were around a form of content streaming to dynamically load character data as needed. For example, the game would initially load only the Light form of Lux. If the player chose to evolve into the Fire form, the game would at that point load all of the data for Fire, and once loaded, perform the graphical switch in-game. Finally the game would unload the information for the Light form. This would allow us to have only two champions’ worth of data loaded at any given time. With two forms loaded at any time, we were now looking at 40 megabytes instead of 200 - much closer to our target of 30.

But ultimately, there were too many problems with this solution. First, since file I/O is an expensive operation, we risked a severely degraded experience for players with lower end hardware. Second, we don’t currently have this functionality in League of Legends, so we would have had to build a completely new system with very little time. What if it didn’t work and we were a month out from delivery and vastly over budget?

We took a step back and looked at our content creation pipeline with fresh eyes. Why did most of our skins need 20 megabytes of in-game memory? And where does all of that memory go for something relatively simple like a game character anyway?

The Flaws of Freedom

We give artists a lot of freedom to create content, which is a nice way of saying that engineering hasn’t provided great tools and validation around said content in the past. We have animation compression , but there is no validation or warning when an animation is created without it. There’s nothing stopping an artist from using a 1024x1024 texture with full alpha support on an effect that is a couple of pixels wide and completely opaque.

With the wider adoption of the Game Data Server , we are approaching a world where we can much more easily validate data automatically, but we aren’t there yet. For Lux, the process had to be a bit more manual. We worked closely with artists throughout the creation of Elementalist Lux to ensure that animations were compressed when possible, textures were the correct size and format, and models were created efficiently without excess polygons.

We were making great progress, but we were still way over budget, and there was one obvious culprit: visual effects.

VFX

Visual effects (VFX) are the most resource-intensive type of content for our champions and skins. They use textures, models, animations, sounds, and there are typically a lot of them. Elementalist Lux was on a whole new level: we’d need 10 times the number of effects used in the average skin.

Your browser doesn't support video.

Please download the file: video/mp4

Of the various components that make up VFX, texturing all of these effects was going to be the biggest offender in terms of memory, so we tackled textures first. We developed technology that allowed artists to use packed palletized textures, giving them the means to express their vision while cutting texture memory size by about two thirds.

Instead of using one fully colored texture file to represent an effect, a packed texture allows the artist to use each color channel of the texture to author a grayscale image.

Elementalist Lux’s ten forms wouldn’t pop quite as much if they were all gray, so the artists also needed a way to add color. This is where the ‘palette’ comes in. A small palette texture serves as a ‘lookup table’ to map grayscale/brightness values between 0 - 1 into a color of the artist's choosing. This palette texture is generally shared amongst lots of different effects, reducing memory usage for identical colors.

The VFX artists took advantage of these techniques to severely reduce the memory overhead of all the of Elementalist Lux’s effects.