Thursday, December 3, 2020

The Legend of Bum-Bo!

Chase down your beloved coin and take out the trash, in this roguelike puzzle adventure!  We helped TheLabel Games bring Legend of Bum-Bo to Android, where it's available as part of a Play Pass subscription!  Trek through the sewers, brave a wide variety of poops, and show that mysterious jerk that nobody can scare Bum-Bo!

Available on Google Play here!

Monday, October 26, 2020

Universal Windows Platform and Unity

Working on a UWP title for Unity, we've run into several obscure issues that may help some other people out.  First off, here are the full Xbox Live plugins (grab the linked .unitypackage):

There's a helpful link in there about setting up more detailed Unity features for UWP and Xbox Live.  The biggest thing there for us was the note about setting the "InternetClient" capability in the UWP publish settings inside Unity PlayerSettings.  Without this, trying to unlock achievements will give you a cryptic error message: "The application does not have the cloud notification capability"

Also useful is getting multi-user support, which will allow you to support the user picker on appropriate platforms.  Specifically, you have to edit the Package.appxmanifest file after building your UWP solution to add "<uap:SupportedUsers>multiple</uap:SupportedUsers>" to the Properties section.  It's briefly mentioned in the "Building and Testing UWP" section of this page.

Friday, October 16, 2020

The Collage Atlas!

A beautifully hand-drawn journey though surreal landscapes, the Collage Atlas is the creation of John Evelyn, and we helped bring it to Apple Arcade!  A must for any fan of meaningful adventure and storytelling!

If you're a subscriber to Apple Arcade, you can find it on the App Store here!

Thursday, October 15, 2020

Ring of Pain!

Do you like cards?  Do you like dungeon crawls?  Do you like creepy owls with elongated limbs?

Ring of Pain is a card-based rogue-lite dungeon crawl with unique and interesting gameplay, and we helped bring it to Nintendo Switch!  Every run has important decisions to make about your path, equipment, and abilities, and the beautiful card artwork and atmosphere really helps set it apart.  Like all the best entries in the genre, you'll discover a lot of intriguing depth as you learn the game.  Oh, and you'll encounter creepy owls with elongated limbs, too!  There's that.

Check it out on Steam or on Switch!

Wednesday, September 23, 2020

Wintermoor Tactics Club on Switch, PS4, and Xbox!

Now available!  If you're a fan of tactics games and are in the mood for a fun and lighthearted narrative, Wintermoor Tactics Club might be just what you're looking for.  We helped bring it to all three consoles!

Pick it up digitally on SwitchPS4, or Xbox!

Thursday, August 13, 2020

Light the Universal Render Pipeline to Resemble the standard Unity renderer

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.


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);

  lightRangeSqr = -1.0 * lightRangeSqr / 0.36;

  // 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


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.


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.