Elementalist Lux: 10 Skins in 30 Megabytes
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.
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.
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.
After textures were optimized, we expected to be close to our 30 megabyte memory budget. Better yet, this technology would unlock similar savings for future skins.
Mission Accomplished, Right?
Celebrations were had, but when we booted up the memory report, we were surprised to see that Elementalist Lux was still 20% over budget! With under a week left until Lux hit PBE, we had to find and fix the discrepancy before it was too late.
Some quick sleuthing using our memory reporting tools showed that we were allocating over 10 megabytes of in-game memory to effects without even accounting for textures. This is an absurdly high number which exposed a significant memory problem in our VFX system.
Our effects system has been described as, “a fixed function tractor driving at 200 miles per hour.” Put another way, every emitter has the capability to do every possible effect, which means every emitter pays the memory price for every possible effect, regardless of its complexity. Back in the day, we built a straightforward particle system that met our needs at the time. We never really had to account for the memory cost, because normal skins usually have a small number of emitters, helping them stay under budget. With Elementalist Lux however, we were loading upwards of 3000 emitters, and her memory cost quickly ballooned.
We quickly identified Animated Variable types as a likely target for savings within our emitter definition. Particle systems often have the ability to animate a variable so that the variable itself changes over time. An example of this would be particle spawn rates. Maybe at the beginning of an effect, the artist wants an emitter to spawn a lot of particles, but a few seconds in, the spawn rate should slow down. Of course storing this animation data takes up memory, but we were surprised to find that our emitters were taking up a lot of memory just storing the ability to do variable animation. Here is a breakdown of some common data types and their League of Legends specific animated counterparts:
Each emitter definition has around 50 animated variable types, and it turns out that over 95% of these variables were constant and didn’t need animation capabilities. Multiply this by the 3000 emitters Elementalist Lux uses, and it’s possible to see how the waste started to add up.
A simple change was implemented to the underlying animated variable structures to only allocate the memory needed for animation data if it existed. This change brought Elementalist Lux under budget, and saved another 10-15 combined megabytes in other places such as the environment, other champions, items, etc.
The Future of Skins
Profiling and digging deep into the effects system taught us a lot about what is going on under the hood when it comes to particles. While we still have more work ahead to modularize the emitter system such that each effect pays the memory cost for only what it uses, this was a great first step that helped bring Elementalist Lux under budget and saved memory overall throughout the game. Outside of the effects system, we also identified several other opportunities to improve our use of memory in champion and skin development, including cleaning up asset debt and unifying all models into a single compressed structure and exporting all of the old animations into a lossless compressed format.
Elementalist Lux served as a reminder of an earlier time at Riot when skins were simpler and optimization was a stretch goal. As I mentioned above, none of the changes we made were revolutionary. Instead, we’re taking iterative steps toward better tech and a better development process as we ship new content. With each project, we can fortify our tools and processes, make life easier for future Rioters, and deliver a better experience to players.
I hope you enjoyed a brief look into some small changes we made to unlock big things with Elementalist Lux. If you have any comments or questions, please leave them below!