Thursday, August 13, 2020

Light the Universal Render Pipeline to Resemble the built-in Unity render

We're working to port a PC Unity title to the mobile platforms, and it's decently expensive on the GPU end.  We did some experimenting and found that we could get a significant speed increase by moving to Unity 2019.3 and the Universal Render Pipeline.  For the most part, doing this conversion went okay.  We were able to upgrade materials and write some scripts of our own to translate others.  The major sticking point was the lighting.  The game uses a lot of spot lights and point lights.  We didn't want to go and tweak the range and intensity of all of them if we could help it, so it made sense to investigate whether we could modify the URP to get its lighting behavior to more closely match the built-in Unity renderer where these lights had already been set up.

TLDR

Modify the distance attenuation calculation in Lighting.hlsl:

float DistanceAttenuation(float distanceSqr, half2 distanceAttenuation)
{
  // Reconstruct the light range from the Unity shader arguments
  float lightRangeSqr = rcp(distanceAttenuation.x);

#if !SHADER_HINT_NICE_QUALITY
  lightRangeSqr = -1.0 * lightRangeSqr / 0.36;
#endif

  // Calculate the distance attenuation to approximate the built-in Unity curve
  return rcp(1 + 25 * distanceSqr / lightRangeSqr);
}

Modify the intensity calculation in ForwardLights.cs:

void InitializeLightConstants(NativeArray<VisibleLight> lights, int lightIndex, out Vector4 lightPos, out Vector4 lightColor, out Vector4 lightAttenuation, out Vector4 lightSpotDir, out Vector4 lightOcclusionProbeChannel)
{
  // .... Unity code

  // We're squaring the intensity because it's a closer match to the built-in Unity renderer
  lightColor = lightData.light.color * lightData.light.intensity * lightData.light.intensity;

  // .... Unity code
}

Background

All this work is being done in Unity 2019.3.10f1 with the Universal Render Pipeline package 7.3.1.  Before we could get to actually doing the modifications, we needed to make sure the modifications would stick around, since just installing the URP package would cause any local changes to be reverted each time Unity started.  This forum post gave us the direction we needed.  Moving the contents of the URP package into the Unity Packages folder let us add them to our source control so we could track our modifications relative to the original version from Unity.

Distance Attenuation

Unity does point out that the lighting distance attenuation is different between the built-in renderer and the Universal Render Pipeline.  That gave us the lead we needed for what to search for.  This forum thread discusses doing what we talked about and suggests a formula to approximate what the built-in Unity renderer does.  The formula they came up with is this (where "d" is the distance from the light to the surface):
1 / (1 + 25 * d^2)
The actual distance attenuation is calculated in the URP shader include <path to URP package>/ShaderLibrary/Lighting.hlsl, in the function DistanceAttenuation.  Since it's using proper physical light behavior, the attenuation is more simply:
1 / d^2
Unity does some additional smoothing math to make sure the attenuation goes all the way to 0 at the edge of the light range.  However, that's dependent on the inverse square calculation of the attenuation.  If we change that, the smoothing no longer works right.  Just plugging in the provided squared distance value to the new formula causes all the lights to be much, much darker.  We weren't sure if this was because the formula was wrong or because we were doing our inputs to the formula wrong.  We had to do some isolated testing.

We set up two test projects in Unity 2019.3.10: one with the built-in Unity renderer, and one with the Universal Render Pipeline.  Both projects were set to use linear color space.  We then made a simple scene in each, matched as closely as possible.  One point light, white color, range 10, intensity 1.  A standard cube, scaled on the X/Y plane so it would fill the screen and have a surface for the light.  Material on the cube is the standard Unity shader (or URP lit shader), color black.


From here, we moved the light position between 0.5 and 10 units away from the surface so we could see how the color value changed as the distance changed.  We got eleven screenshots that we could sample colors from:

This results in the following values:

 Light Distance | Built-in Unity Renderer Color (0-255)
 0.5            | 125
   1            | 116
   2            |  94
   3            |  75
   4            |  61
   5            |  51
   6            |  44
   7            |  39
   8            |  34
   9            |  24
  10            |  13

Which look like this:


Plugged into a program to find an equation to fit the curve, it lines up almost exactly with the formula we were looking at before (1 / (1 + 25 * d^2)).  So the formula is right, which means our input was wrong.

The square distance given to the distance attenuation function is raw.  It's not scaled to match the light range.  The most obvious thing to try was to bring the distance back into range.  Any distance value greater than the light range should automatically attenuate to 0.  So rather than have the "d" in the formula be the distance, let's have it be the percentage of the light range that this distance covers (distance divided by light range).  If the distance is greater than the light range, that should just make the final attenuation that much smaller, since it's a reciprocal.

Looking in <path to URP package>/Runtime/ForwardLights.cs in the method InitializeLightConstants, we see the values sent to the shader are calculated as:

float lightRangeSqr = lightData.range * lightData.range;
float fadeStartDistanceSqr = 0.8f * 0.8f * lightRangeSqr;
float fadeRangeSqr = (fadeStartDistanceSqr - lightRangeSqr);
float oneOverFadeRangeSqr = 1.0f / fadeRangeSqr;
float lightRangeSqrOverFadeRangeSqr = -lightRangeSqr / fadeRangeSqr;
float oneOverLightRangeSqr = 1.0f / Mathf.Max(0.0001f, lightData.range * lightData.range);

// On mobile and Nintendo Switch: Use the faster linear smoothing factor (SHADER_HINT_NICE_QUALITY).
// On other devices: Use the smoothing factor that matches the GI.
lightAttenuation.x = Application.isMobilePlatform || SystemInfo.graphicsDeviceType == GraphicsDeviceType.Switch ? oneOverFadeRangeSqr : oneOverLightRangeSqr;

lightAttenuation.y = lightRangeSqrOverFadeRangeSqr;

We are looking at lightAttenuation.x specifically.  That's what gets passed into the shader function as distanceAttenuation.  In the case of non-mobile platforms, inverting it gives you the square light range.

float lightRangeSqr = rcp(distanceAttenuation.x);

However, for mobile platforms, the value is further altered so they can do a faster calculation in the original attenuation function and get a smooth attenuation at the edge of the light range.  Reversing the equations for fade start distance and fade range, you wind up with the extra adjustment for mobile only:

lightRangeSqr = -1.0 * lightRangeSqr / 0.36;

Now that we have a valid square light range, we can use it to divide the square distance and come up with a normalized value that we can plug into the original formula:

rcp(1 + 25 * distanceSqr / lightRangeSqr);

If we then repeat the point light test that we did for the built-in Unity renderer, we can record a similar set of values, and the comparison between the two comes out to:


The lines match almost exactly.  There is a slight difference in the last 20% of the light range.  Unity does a smoother fade to 0, but this formula does not take that into account.  Most of the lights in our game have their range extend beyond what's visible to the player, so we decided it wasn't worth correcting this.

After verifying the distance attenuation was solid, we made sure to check the angle attenuation for spot lights.  We rotated the light around the cube and checked color values with the built-in Unity renderer and the URP with the corrected distance attenuation.  Luckily, these values matched up properly, and no further adjustments were needed.

Intensity

After validating the distance and angle attenuation were correct, we still saw a decent amount of difference between the lighting in our game before and after the Universal Render Pipeline conversion.  We decided to investigate the intensity.  Using the same test setup as before, we kept the light at a fixed distance (2 units) and varied the intensity from 0 - 3. (Color values are 0-255, URP Color is measures with the distance attenuation fix in place.)

Light Intensity | Built-in Unity Renderer Color | URP Color
  3             | 255                           | 154
2.7             | 250                           | 147
2.4             | 224                           | 139
2.1             | 196                           | 131
1.8             | 169                           | 122
1.5             | 141                           | 113
1.2             | 112                           | 102
0.9             |  84                           |  89
0.6             |  55                           |  74
0.3             |  29                           |  54
  0             |  13                           |  14

Graphed, this looks like:


This was unexpected, but after experimenting for a bit, the best correction we were able to find was to square the intensity value when applying it.  The light intensity is computed into the color already by the Unity engine in UnityEngine.Rendering.VisibleLight.finalColor.  However, all that's doing appears to be multiplying Light.color by Light.intensity.  So back in ForwardLights.cs, we modified the method InitializeLightConstants to define the lightColor output parameter as:

lightColor = lightData.light.color * lightData.light.intensity * lightData.light.intensity;

Returning to the testing, this seemed to match the built-in renderer values fairly closely for the same test case.

Thursday, July 30, 2020

Fixing Git after Moving a File Type to LFS

We ran into a problem a while back where we had a Git repo that had been in use for a while and then we decided one of the common file types should be in the LFS.  The obvious way to do this was to update the filters in the .gitattributes file in the root:

*.ext filter=lfs diff=lfs merge=lfs

However, after doing this, the various Git clients (command line, Source Tree, etc.) didn't actually move all the *.ext files into the LFS.  Any new commits we made that touched those files would automatically move them, but there were plenty of files that didn't get updated.  We noticed this because Git would notice some of these other files (even though they hadn't been modified) and give us the dreaded "Encountered X file(s) that should have been pointers, but weren't" error whenever doing Git operations.

A little Goolging led us to this method of getting the rest of the files into the LFS:

git lfs migrate import --no-rewrite path/to/file.ext

This seemed pretty straightforward.  We wrote a Python script that did an os.walk() across our project and compared it against the results of git lfs ls-files to see which of the new files had yet to be added to the LFS.  This came up with a couple hundred files for us.  We had the script also run the import command above and with that we had the remaining files where they needed to be.  Push to remote, and we're set.

We were not set.

When we attempted to view the Git repo in Source Tree and also Git Fork, the program either crashed or hung spinning on a load.  Somehow we had broken the Git repo.  We reviewed the logs in the command line client and noticed that the timestamps on all the import commands were December 31st, 1969.  Effectively a zero value in the date/time field for the commit, which seemed to confuse the graphical Git clients.  Back to Googling.

This StackOverflow answer from Ortomala Lokni provided a potential solution.  We needed to modify the process a bit for the Windows machines we were on.  Basically, the date setting needed to be two commands: set the committer date as an environment variable once:

set GIT_COMMITTER_DATE="2020-07-19T00:00:00"

Then we could proceed with the individual commit updates during the rebase:

git commit --amend --date="2020-07-19T00:00:00"

Of course, a major inconvenience here is that we still have the .gitattributes filter set to *.ext files should be in the LFS.  And when we do the rebase process, it takes the files out of the LFS which goes against the filter specification, and thus we can't amend the commit because of the filter error.  Luckily, Git has the ability to override the .gitattributes list temporarily by placing a file called "attributes" (no extension) in the ".git/info" folder in the project.  In that file we place the override entry:

*.ext !filter !diff !merge

This effectively makes Git ignore that file's LFS filter so the rebase can proceed without errors.  However, once the rebase was complete, we had to remove that override file again before resuming normal operations.

Friday, July 17, 2020

Forager!

Forager is a fun and colorful open world exploration, farming and crafting game!  We worked with HopFrog and Humble to update Forager on both Switch and PS4, bringing all the new content over the last several months to existing console players.  Plus, we brought the game to Xbox One, where it's now available via Xbox Game Pass!




If you're a fan of Zelda-style exploration, or enjoy building up your farm in Stardew Valley, you should give Forager a look.  If you prefer your PC, it's also available on Steam!

Thursday, May 7, 2020

Wintermoor Tactics Club!

Now available on Steam!  Inspired by Final Fantasy Tactics, Wintermoor features a fun story and great turn-based tactical battles with interesting character abilities that really make you think.  We worked with the folks at EVC and Versus Evil to help with UI work and platform support, and you'll find the game on consoles in a few weeks.


Meanwhile, if you prefer PC, snag it on Steam here!

Friday, May 1, 2020

Neversong!

Neversong is a beautiful and atmospheric platformer that explores themes of loss and life.  Help Peet find his way out of a nightmare dream world!  Discover what led to its creation!  Fight bird monsters with a baseball bat!  It's published by Serenity Forge, and we helped bring it to Apple Arcade platforms.  Available now!

Neversong, the narrative-driven adventure game set in a stylish nightmare, launches on Apple Arcade

If you're an Apple Arcade subscriber, you can get Neversong for your iOS or macOS device via the app store, via these convenient links!

Saturday, March 21, 2020

Free Controller Prompts

I spent the weekend making a full set of button prompts for all the current console controllers, and figured I'd release this for free for anyone who's got a use for them.  These should be fully compliant for certification if you're shipping a console game, based on our experience there.  They're high contrast and sharp looking and should fit in with any kind of modern or minimalist game-feel.

These should give you everything you need for Switch, PS4, Xbox One, Xbox 360, and PS1/2/3 prompting.  There's also a couple blanks for style-matching keyboard icons if you need them.

The package includes vector and high-res raster artwork (both as a power-of-two sheet and as a set of individual 128x128 images).  There's a preview at the OpenGameArt listing if you're interested, or get the zip file directly here.

EDIT April 5th: Now includes the full keyboard, as well.

Thursday, March 19, 2020

Congrats to the Mutazione and What the Golf teams!

We helped bring What the Golf to Apple Arcade, and Mutazione to Apple Arcade and PS4.  In the last few days, What the Golf has won Best Mobile Title at the 2020 Game Developers Choice Awards, and Mutazione has taken home the Excellence in Audio award from the IGF Awards!

Well done to both teams!

Tuesday, March 10, 2020

Everything in GameMaker is an Index

GameMaker as an engine has a lot of positives. It's friendly to start out with, it has surprisingly good multi-platform support, and it has nice pixel-perfect rendering for 2D titles.  But it's got some real down-sides too, including the fact that GML, its scripting language, seems very organically grown. It has the loosest syntax rules of anything I've ever seen -- Want brackets?  Want semicolons?  Want parentheses?  Up to you, it's all optional!  Typing is loose, though types do exist, and helper methods exist for lists and dictionaries.  It doesn't have classes, only functions.

One of the less obvious quirks of GameMaker doesn't become apparent until you work with it for a while, though, and that's what I wanted to discuss in this article.  This is the fact that everything, under the hood, is an index.

We recently tracked down a bug in one of the projects we've been working on, and the summary was this: Sometimes when you destroy a barrel during gameplay, it'll crash.  There's a error message to go along with this indicating it's inside an Achievements related script, and the message simply says that the function expected a number.

Understand that 'sometimes' here is more like 'rarely'.  You can go destroy these things dozens of times without a problem, and in fact we had so much difficulty reproducing this that we'd basically given it up as just one of those things you end up letting slide.  We'd put in safety checks on all the achievement related methods making sure the arguments were typed correctly, but really there weren't that many inputs and it made no sense to us that a non-number was slipping into the list of achievement values.

Until I finally happened to catch the crash in a debugger.  Upon inspecting the list of achievements to see what non-number was present, I was surprised to discover that instead the list of achievements had transformed into a completely different list.  This list was shorter than the actual list of achievements, and was full of numbers ranging from single-digits up to around 250.  The not-a-number crash was the achievement save process running off the end of the too-short list and ending up with null values -- GameMaker doesn't do overrun checking.  Tracking the cause of this problem took all day, and included steps like writing wrappers for GameMaker's add-to-list methods that would log what object is doing the addition, writing detectors to report if the achievements array's length ever changed, careful review (again) of everything interacting with achievements at all, tons of gameplay trying to recreate the problem, etc.

In the end, here's what happens:

The player shoots a barrel.  The barrel shoots off in a direction, plays a particle effect and sound, and explodes.  When it does this it adds the particle effect to a list of effects called particle_list, and these get cleaned up later.  Sounds good, until you look at other uses of particle_list and realize that it's not a list, it's actually a dictionary.

But that's the thing, particle_list isn't a dictionary either, it's an INDEX to a dictionary.  It's pointing at, say, dictionary #7.  And when you call the AddToList method and tell it to add something to particle_list, it has no idea what you should have used AddToDictionary instead.  It goes and dutifully adds your value into list #7, and now you've edited an unknown list.

So, the barrel adds that particle effect to a list at particle_list's index, instead of a dictionary at particle_list's index.  Sometimes this accidentally-selected list is something harmless, sometimes not, but in the rare case of our crash it just happened to get added to a list of other lists, and that particular list-of-lists is used to do cleanup at the end of the level.

Now, understand that everything in GameMaker is an index.  This includes references to assets, references to instances of objects in the scene, references to the environment you're in, etc.  So adding the particle effect to that list of lists is just adding another index to a list of indexes.  The game doesn't have a ton of particle effects, so often the index for the in-world particle effect was a very low number.  Sometimes, that index was 1.

Upon launching the game, one of the first things it does is initialize the list of achievements and assign it to global.AchievementList.  This means that under the hood, the achievements list always had a very low index, in fact it was always 1 in my testing.

So, through coincidence of the particle effect's index, and through coincidence of the particle_list's current index, that barrel had just added the index of the achievements list to the list of lists to get cleaned up.  For the time being this is harmless, at least in the sense that you just end up with a done-playing particle effect being left in the scene.  However, eventually the player will finish the level, the cleanup script will run, and now the achievements list has been deleted.

Upon entering the next level, the game builds a series of lists for things like which enemies are allowed to spawn, which weapons can spawn, and so on.  Since the slot the achievements list had formerly occupied is now vacant, one of these lists ends up using its space.  As the global.AchievementList variable is just an index, it has no idea about any of this, it remains happily pointing at the now-replaced slot #1.  This is how the achievements list had been replaced with a different one.  The next time the player progresses any achievement and the game tries to write out the progress, it will crash when it overruns the now-incorrect list.  And, sometimes, it maybe won't crash because the list is long enough.

Something to understand here is that this bug can do so many things that don't result in a crash.  All of the various indices that GameMaker uses start their count at zero, so it's not really a stretch that the index of a particle effect is also a valid index for a list.  There will also be a sprite #1, an object #1, a sound #1, a dictionary #1, and so on.  Almost any erroneous mis-match of an index could technically be valid and so this bug remained silent for a very long time.  At times it likely resulted in things like incorrect objects being spawned into the level, or incorrect rewards being given.  All manner of "I saw a weird thing but it never happened again" bugs could come from this simple accidental usage of AddToList instead of AddToDictionary.

Beyond that, this is an easy error to make (and the variable name didn't help things, here).  It was also time consuming to diagnose, very disproportionate to how easily someone could make the mistake.  If you have a suspicion this is a problem, you'd need to manually audit usage of all your lists/dictionaries to confirm things were being used correctly.  Or create a wrapper for the addition functions that give you a breadcrumb trail, and begin tracking down the histories of usage, like we did.

This was an interesting issue to track down and something that really could only happen in GameMaker in my experience, and seemed worth explaining.  Hopefully it reminds any GameMaker users out there to be careful and be aware of the possibilities of this kind of mis-match.

Friday, March 6, 2020

One Step From Eden!

We helped bring this great title to the Switch, and it'll be on sale March 26th!  Or if you prefer PC, pick up the Steam version!

Image result for one step from eden

One Step From Eden is a fast and fun action/deckbuilding game.  Earn new abilities to customize your character and progress through a rogue-lite adventure.  If any of that seems up your alley, make sure to check it out!

EDIT: Available now on Switch!