Skip to main content
Devlog

A Mario Galaxy Look on a Phone: fog culling, sky fog, automatic quality, baked shadows

A Kugle level in the game: a green planet in a purple sky, and behind it another planet and a tube already fading into the colour of the sky

The target: the look of Super Mario Galaxy, small round planets, soft shadows, a sky that swallows the horizon. The catch: 60 FPS on a phone, in one Unity project with the PC version.

  1. Fog culling: what is far is not drawn, a fog hides the cut.
  2. A fog the colour of the sky behind each pixel.
  3. One switch per platform, no code.
  4. Automatic quality: the game measures its FPS and adjusts.
  5. Box mapping: textures without UVs.
  6. Moving cloud shadows: one texture, nothing on the CPU.
  7. Shadows baked in the vertices: soft shadows for free.

1. Fog culling: a far distance per layer​

The Per Layer Far Distance component on the camera: one slider per layer of the project, most of them at 80, the PostProcessing and Far layers left at the camera's 2000

PerLayerFarDistance on the camera: one slider per layer, the distance where it stops being drawn. Unity has the property (Camera.layerCullDistances) but no inspector: this is it.

The fog is complete at 60, gameplay layers are cut at 80: what disappears was already sky-coloured. Nothing pops.

Low FPS on a phone? Lower a slider. Nothing to bake, zero cost at runtime.

Show the code
[ExecuteInEditMode]
public class PerLayerFarDistance : MonoBehaviour
{
[SerializeField] private Camera _camera; public Camera Camera { get { return (_camera); } }
[SerializeField] private float[] _distances = new float[32];
[SerializeField] private bool _useRoundFar = false;

private void OnEnable()
{
SetupCullings();
}

private void SetupCullings()
{
if (_camera == null)
{
return;
}
_camera.layerCullDistances = _distances;
if (GraphicsSettings.currentRenderPipeline == null)
{
// Spherical layer culling exists only in the built-in RP (an SRP ignores it and logs a warning).
_camera.layerCullSpherical = _useRoundFar;
}
}

private void OnValidate()
{
SetupCullings();
}
}

2. A fog the colour of the sky​

A Kugle planet far from the camera: its ground and clouds are one flat purple, darker than the sky above and lighter than the sky below

Our sky is a gradient, not one colour: a single fog colour is too light above the horizon and too dark below. You can see the trick.

The same view with the sky fog: the planet and the clouds fade into the sky behind them, darker at the bottom, lighter at the top

The fix: the sky is captured once in a tiny cubemap (32 px per face), and each pixel's fog takes the colour of the sky behind it. One sample per pixel.

Bonus: the Fresnel rim was added after the fog and glowed through it. Moved before the fog: gone in the distance.

Show the shader function
TEXTURECUBE(_KugleSkyFogCube);
SAMPLER(sampler_KugleSkyFogCube);
float _KugleSkyFogBlend;

void KugleSkyFog_float(float3 FogColor, float3 WorldPos, out float3 Out)
{
float3 direction = normalize(WorldPos - _WorldSpaceCameraPos);
float3 sky = SAMPLE_TEXTURECUBE_LOD(_KugleSkyFogCube, sampler_KugleSkyFogCube, direction, 0).rgb;
Out = lerp(FogColor, sky, saturate(_KugleSkyFogBlend));
}

3. One switch per platform​

The Laptop Or Smartphone Execute component: three lists of events, On Enable Laptop, On Enable Smartphone and On Game Server Always, each one turning the objects OnlyOnWindow and OnlyOnPhone on or off

LaptopOrSmartphoneExecute: three event lists, laptop, smartphone, game server. Drop it on a prefab, drag the objects, tick: shown on PC, hidden or lighter on phone. No code, the whole team uses it.

It follows the quality setting, not the build: it switches live, mid-game.

4. Automatic quality​

The game averages its FPS after each loading and steps the quality down when it drops, back up when it holds 60.

Strong deviceWeak device
Render scale2 on PC (supersampling)0.8
QualityVery HighVery Low: no shadows, lighter textures and LODs
Visualsfull volume, temporal anti-aliasinglight volume and visuals, fast anti-aliasing

5. Box mapping: textures without UVs​

Spheres, tubes, bent platforms: no UV unwrapping. The texture is projected from the world, continuous from one object to the next: a level designer drops any primitive, it is textured at once.

6. Cloud shadows: a moving black noise​

A black noise sliding with time over the ground: clouds drifting over the planet, or a wet ground when it rains. One texture sample, nothing on the CPU.

7. Shadows baked in the vertices​

Two copies of the same rock: on the left the new bake, grey under where a nest sits and in the gaps between the stones; on the right the old bake, shaded only by the direction of its faces

No shadow maps on a phone. Each object is baked once in the editor: its ambient occlusion goes into the vertex colours, and the shader multiplies by it. Free at runtime.

Unique bake: the nest sat on a rock with too few vertices to hold its shadow. One click subdivides that instance only (300 to 8,400 vertices) and bakes just the nest's shadow on top. 22 seconds.

What I keep from it​

  • Bake what does not move: the phone only multiplies and blends.
  • Tools for the team, not tickets for programmers: sliders, switches, bake windows.
  • One project, two platforms, and the game picks its own quality.

See also​